1

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.Func2[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)

mickl
  • 48,568
  • 9
  • 60
  • 89
nightblade9
  • 354
  • 2
  • 15
  • What is the type of `EmailAddress`? Just out of curiosity, what if you try simplifying to `var toReturn = objects.Find(predicate).SingleOrDefault();` I don't know how it builds queries, but I wonder if it doesn't like that you're invoking the predicate rather than just providing it as a parameter. – Scott Hannen Apr 22 '20 at 20:34
  • @ScottHannen `EmailAddress` is just a string. I think `objects.Find(predicate)` doesn't compile because it's trying to convert `System.Something.Expression>>` to `Func`. – nightblade9 Apr 22 '20 at 20:45
  • What about converting the `Func` to an Expression before you pass it in? https://stackoverflow.com/questions/767733/converting-a-net-funct-to-a-net-expressionfunct – Scott Hannen Apr 22 '20 at 20:48
  • @ScottHannen that results in the same exception. – nightblade9 Apr 24 '20 at 16:06

1 Answers1

1

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.

mickl
  • 48,568
  • 9
  • 60
  • 89
  • Your explanation makes sense. Aside from the problem of having to pass in a `Func` instead of a predicate, this code doesn't compile - `predicate(o)` doesn't work for `Func`. Also, see Scott's comments above: converting to an `Expression>` throws the same exception. – nightblade9 Apr 24 '20 at 16:09
  • @nightblade9 that's my bad, sorry - don't run the predicate just pass the expression. Modified my answer – mickl Apr 24 '20 at 16:11
  • 1
    Whoa that actually works! I'm a bit surprised (I was doing something similar based on the comments - just internally converting `Func` to `Expression>` and it throws). Thanks so much for this! – nightblade9 Apr 24 '20 at 16:56