简体   繁体   中英

IMongoCollection<T> throws an when I pass in a predicate for Func<T, bool>

I'm trying to unit-test a ASP.NET Core web controller. Here's what the naive data-access looked like:

var database = configuration.GetSection("MongoDb:Database").Value;
var mongoSettings = new MongoCollectionSettings() { AssignIdOnInsert = true };
var users = client.GetDatabase(database).GetCollection<User>("User", mongoSettings);
var user = users.Find(u => u.EmailAddress == emailAddress).SingleOrDefault();

This works fine. It's not easily unit-testable ( Find is not mockable with Moq), and I would prefer a layer of indirection over my data-access, so I tried refactoring out into a repository class like so:

public interface IRepository<T>
{
    T SingleOrDefault(Func<T, bool> predicate);
}


public class MongoRepository<T> : IRepository<T>
{
    // ...
    public T SingleOrDefault(Func<T, bool> predicate)
    {
        var objects = client.GetDatabase(this.databaseName).GetCollection<T>(this.repositoryName, this.settings);
        var toReturn = objects.Find(o => predicate(o) == true).SingleOrDefault();
        return toReturn;
    }
}

When I update my controller to use MongoRepository<User> instead, it throws:

var usersRepo = new MongoRepository<User>(this.configuration, this.client);
var user = usersRepo.SingleOrDefault(u => u.EmailAddress == emailAddress);

Specifically, in the repository code, objects.Find(o => predicate(o) == true).SingleOrDefault(); throws an exception: An exception of type 'System.InvalidOperationException' occurred in MongoDB.Driver.dll but was not handled in user code: 'Invoke(value(System.Func 2[AutoDungeoners.Web.Models.User,System.Boolean]), {document}) is not supported.'`

I am not sure how to resolve this. I suspect the definition of my predicate as Func<T, bool> is incorrect, because if I just call objects.Find(o => true) , it returns the first object without any issue.

Full exception stack below.

An exception of type 'System.InvalidOperationException' occurred in MongoDB.Driver.dll but was not handled in user code: 'Invoke(value(System.Func`2[AutoDungeoners.Web.Models.User,System.Boolean]), {document}) is not supported.'
   at MongoDB.Driver.Linq.Translators.PredicateTranslator.GetFieldExpression(Expression expression)
   at MongoDB.Driver.Linq.Translators.PredicateTranslator.TranslateComparison(Expression variableExpression, ExpressionType operatorType, ConstantExpression constantExpression)
   at MongoDB.Driver.Linq.Translators.PredicateTranslator.TranslateComparison(BinaryExpression binaryExpression)
   at MongoDB.Driver.Linq.Translators.PredicateTranslator.Translate(Expression node)
   at MongoDB.Driver.Linq.Translators.PredicateTranslator.Translate(Expression node, IBsonSerializerRegistry serializerRegistry)
   at MongoDB.Driver.Linq.Translators.PredicateTranslator.Translate[TDocument](Expression`1 predicate, IBsonSerializer`1 parameterSerializer, IBsonSerializerRegistry serializerRegistry)
   at MongoDB.Driver.ExpressionFilterDefinition`1.Render(IBsonSerializer`1 documentSerializer, IBsonSerializerRegistry serializerRegistry)
   at MongoDB.Driver.MongoCollectionImpl`1.CreateFindOperation[TProjection](FilterDefinition`1 filter, FindOptions`2 options)
   at MongoDB.Driver.MongoCollectionImpl`1.FindSync[TProjection](IClientSessionHandle session, FilterDefinition`1 filter, FindOptions`2 options, CancellationToken cancellationToken)
   at MongoDB.Driver.MongoCollectionImpl`1.<>c__DisplayClass41_0`1.<FindSync>b__0(IClientSessionHandle session)
   at MongoDB.Driver.MongoCollectionImpl`1.UsingImplicitSession[TResult](Func`2 func, CancellationToken cancellationToken)
   at MongoDB.Driver.MongoCollectionImpl`1.FindSync[TProjection](FilterDefinition`1 filter, FindOptions`2 options, CancellationToken cancellationToken)
   at MongoDB.Driver.FindFluent`2.ToCursor(CancellationToken cancellationToken)
   at MongoDB.Driver.IAsyncCursorSourceExtensions.SingleOrDefault[TDocument](IAsyncCursorSource`1 source, CancellationToken cancellationToken)
   at MongoDB.Driver.IFindFluentExtensions.SingleOrDefault[TDocument,TProjection](IFindFluent`2 find, CancellationToken cancellationToken)

The only implementation of Find(...) method you can use here is (docs) :

public static IFindFluent<TDocument, TDocument> Find<TDocument>(this IMongoCollection<TDocument> collection, Expression<Func<TDocument, bool>> filter, FindOptions options = null)

The difference between Func and Expression<Func> was described here . Basically GetCollection returns a reference to Mongodb collection and the role of Find method here is to convert expression tree into MongoDB query. The moment when your query gets executed in the database is when you call SingleOrDefault() and this is where you're going to get an exception.

Since there's no implicit conversion between Func<T,bool> and Expression<Func<T,bool>> you tried to use o => predicate(o) == true which creates another expression and makes your code compileable but as mentioned - MongoDB .NET driver will not be able to figure out how to translate such expression into MongoDB query.

You need to change your implementation into:

public T SingleOrDefault(Expression<Func<T, bool>> predicate)
{
    var objects = client.GetDatabase(this.databaseName).GetCollection<T>(this.repositoryName, this.settings);
    var toReturn = objects.Find(predicate).SingleOrDefault();
    return toReturn;
}

And keep in mind that this will work only for those expression that can be translated into MongoDB query language. u => u.EmailAddress == emailAddress looks fine.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM