15

I've implemented the TestDbAsync fakes from https://msdn.microsoft.com/en-us/library/dn314429(v=vs.113).aspx and I want to be able to use AutoMapper to project to a different type before calling the Async EF methods (ToListAsync, CountAsync, etc.).

I get a cast exception in ProjectionExpression.To

Example code that throws the exception.

_userRepository.GetAll().OrderBy(x => x.Id).ProjectTo<User>.ToListAsync();

This works fine in a non-test scenario, but when I mock the DbSet using the TestDbAsyncEnumerable I get

: Unable to cast object of type 'Namespace.TestDbAsyncEnumerable`1[UserEntity]' to type 'System.Linq.IQueryable`1[User]'.

Right now to get around this I have to ProjectTo after the call to the Async EF extensions. Is there any way to keep the ProjectTo call before the EF extensions?

Reference code:

public class TestDbAsyncEnumerable<T> : EnumerableQuery<T>, IDbAsyncEnumerable<T>, IQueryable<T>
{
    public TestDbAsyncEnumerable(IEnumerable<T> enumerable)
        : base(enumerable)
    { }

    public TestDbAsyncEnumerable(Expression expression)
        : base(expression)
    { }

    public IDbAsyncEnumerator<T> GetAsyncEnumerator()
    {
        return new TestDbAsyncEnumerator<T>(this.AsEnumerable().GetEnumerator());
    }

    IDbAsyncEnumerator IDbAsyncEnumerable.GetAsyncEnumerator()
    {
        return GetAsyncEnumerator();
    }

    IQueryProvider IQueryable.Provider => new TestDbAsyncQueryProvider<T>(this);
}

public static Mock<DbSet<T>> ToAsyncDbSetMock<T>(this IEnumerable<T> source)
        where T : class
    {

        var data = source.AsQueryable();

        var mockSet = new Mock<DbSet<T>>();

        mockSet.As<IDbAsyncEnumerable<T>>()
            .Setup(m => m.GetAsyncEnumerator())
            .Returns(new TestDbAsyncEnumerator<T>(data.GetEnumerator()));

        mockSet.As<IQueryable<T>>()
            .Setup(m => m.Provider)
            .Returns(new TestDbAsyncQueryProvider<T>(data.Provider));

        mockSet.As<IQueryable<T>>().Setup(m => m.Expression).Returns(data.Expression);
        mockSet.As<IQueryable<T>>().Setup(m => m.ElementType).Returns(data.ElementType);
        mockSet.As<IQueryable<T>>().Setup(m => m.GetEnumerator()).Returns(data.GetEnumerator());

        return mockSet;
    }
Dylan Musil
  • 319
  • 1
  • 13

5 Answers5

25

Edit your TestDbAsyncQueryProvider<>.CreateQuery() so that it returns the right type of the expression passed by ProjectTo<>.

Here is my sample implementation.

public IQueryable CreateQuery(Expression expression)
{
    switch (expression)
    {
        case MethodCallExpression m:
            {
                var resultType = m.Method.ReturnType; // it shoud be IQueryable<T>
                var tElement = resultType.GetGenericArguments()[0];
                var queryType = typeof(TestDbAsyncEnumerable<>).MakeGenericType(tElement);
                return (IQueryable)Activator.CreateInstance(queryType, expression);
            }
    }
    return new TestDbAsyncEnumerable<TEntity>(expression);
}

https://gist.github.com/masaedw/95ab972f8181de6bbe48a20ffe9be113

I have written also unit test. It's working.

https://github.com/masaedw/AutoMapper/blob/TestDbAsync/src/IntegrationTests/MockedContextTests.cs

Ian Robertson
  • 2,652
  • 3
  • 28
  • 36
Masayuki Muto
  • 376
  • 3
  • 9
6

I ran into this same problem, in addition to the accepted answer you might also have the generic version of CreateQuery like I do - I fixed that too like this:

public IQueryable<TElement> CreateQuery<TElement>(Expression expression)
{
    var queryType = typeof(TestDbAsyncEnumerable<>).MakeGenericType(typeof(TElement));
    return (IQueryable<TElement>)Activator.CreateInstance(queryType, expression);
}

The type is being provided by TElement, so its a simplier implementation on the generic version.

Ian Robertson
  • 2,652
  • 3
  • 28
  • 36
4

I was getting this same error in my tests after upgrading from Automapper 6.0.2 to 6.1.1. Downgrading back to 6.0.2 fixed the issue.

Not sure if this is a regression or a breaking change in Automapper. I haven't had time to pursue it further than reviewing the change log and github issues. Nothing jumps out.

jslatts
  • 9,307
  • 5
  • 35
  • 38
  • Didn't think to downgrade. We ended up abstracting away the EF calls behind an interface for testing. – Dylan Musil Jul 11 '17 at 01:06
  • As @Masayuki Muto discovered below, the problem is in the mock provider. If anyone is interested in contributing, we could also work around it inside AM. – Lucian Bargaoanu Aug 19 '17 at 08:57
2

By using a combination of the two mentioned answers above helped me use .ProjectTo across our unit tests.

In my TestDbAsyncQueryProvider implementation I replaced these two methods:

public IQueryable CreateQuery(Expression expression)
{
    switch (expression)
    {
        case MethodCallExpression m:
            {
                var resultType = m.Method.ReturnType; // it shoud be IQueryable<T>
                var tElement = resultType.GetGenericArguments()[0];
                var queryType = typeof(TestDbAsyncEnumerable<>).MakeGenericType(tElement);
                return (IQueryable)Activator.CreateInstance(queryType, expression);
            }
    }
    return new TestDbAsyncEnumerable<TEntity>(expression);
}

public IQueryable<TElement> CreateQuery<TElement>(Expression expression)
{
    var queryType = typeof(TestDbAsyncEnumerable<>).MakeGenericType(typeof(TElement));
    return (IQueryable<TElement>)Activator.CreateInstance(queryType, expression);
}

It works fine with the latest EF6 as of today.

Ilias.P
  • 179
  • 7
1

in case someone on his Entities has other object, the ProjectTo will crash or throw an exception with Null Reference Object.

Solution for that, is to make sure your Entity has all its objects initialized with a default() or something.

Answer found on the last comment / answer here -> Why is Automapper.ProjectTo() throwing a null reference exception?

P.S thanks for the IAsyncQueryProvider implementation :)

Mircea Mihai
  • 167
  • 1
  • 13
  • Thank you. It took me a long time to find this solution, so here is something for Google to latch onto for people in the future: System.NullReferenceException Object reference not set to an instance of an object. at lambda_method(Closure , UserEntity ) at System.Linq.Enumerable.WhereSelectListIterator`2.MoveNext() at System.Linq.Enumerable.Count[TSource](IEnumerable`1 source) at TestDbAsyncQueryProvider`1.Execute[TResult](Expression expression) – Onosa Sep 29 '21 at 19:23