MockQueryable icon indicating copy to clipboard operation
MockQueryable copied to clipboard

Option to not load the navigation property

Open leopripos opened this issue 3 years ago • 8 comments

Do we already have this feature?

If no, I think this is necessery to make sure that Include statement is called or not in my repository or query. The sample scenario:

public class Lesson {
  public int Id {get; set;}
}

public class Student {
  public int Id {get; set;}

  public List<Lesson> Lessons   { get; set; }
}

var student1 = new Student(){
  Id = 1,
  Lessons = new List<Lesson>(){
    new Lesson(){
      Id = 1,
    }
  }
}

var students = new List<Student>(){
  student1
};

var dataSetMock = students.AsQueryable().BuildMockDbSet();

var dbContextMock = new Mock<IDbContextInterface>();
dbContextMock.SetupGet(e => e.Students)
    .Returns(dataSetMock.Object);

var repository = new StudentsRepository(dbContextMock.Object);

var studentResult = repository.GetById("12312312");

studentResult.Lessons.Should().NotBeNull();

But this will causes studentResult == student1 returns false because we need to instansiate new object.

My suggestion for this, we can add new param in BuildMockDbSet

public static IQueryable<TEntity> BuildMock<TEntity>(this IQueryable<TEntity> data, bool loadNavigation= true) where TEntity : class {}

Thanks :)

leopripos avatar Jan 22 '21 04:01 leopripos

Hi @leopripos, thanks you for the feedback. I would really like to help you.

What happening in repository.GetById ? Why do you expect that repository.GetById("12312312") will create a new object?

romantitov avatar Jan 22 '21 13:01 romantitov

Closing the issue, since there is not enough details to reproduce it

romantitov avatar Jan 28 '21 07:01 romantitov

Sorry @romantitov for the slow response. My bad.

The issue is "How do I make sure (test) that the Lessons navigation property is included or not in my repository" Here is the repository: Good version

public Student GetById(string id)
{
      return _dbContext.Students
                .Include(e => e.Lessons)
                .Where(e => e.Id == id)
                .FirstOrDefault();
}

In the test code above, studentResult.Lessons.Should().NotBeNull(), will always success even I write my repository without Include like this: (Bad version)

public Student GetById(string id)
{
      return _dbContext.Students
                .Where(e => e.Id == id)
                .FirstOrDefault();
}

That's because, it returns the student1 object which was used when creating DbSet mock. And yes, the Lessons property of object student1 has value, not null.

leopripos avatar Jan 29 '21 06:01 leopripos

@leopripos I think the problem is that MockQueryable creates an in-memory queryable (Linq to Objects), but Include is an Entity Framework feature, which translates to SQL joins. Since the queryable isn't an EF Core queryable, it doesn't know how to handle Include. Unfortunately I don't think it would be possible to support Include in MockQueryable...

If it were possible, it would be awesome... I just ran into an issue with a filtered include (EF Core 5), and I'm now realizing that MockQueryable can't do anything about it.

thomaslevesque avatar Feb 02 '21 14:02 thomaslevesque

@leopripos, thank you for more detailed explanation. I agree, that would be a very useful feature. I even did some research and I think technically it is possible (with some changes) to see whether an Include executed or not. But since Include is an extension it is not so easy to verify an execution of the extension with frameworks like Moq. Unfortunately the feature is not something what can be easily done and will take some time.

I usually work with the project in my free time and now I'm quite busy with my commercial projects for now.

I'll keep the issue opened and who knows, maybe one day me or someone else will add the feature to the project. But I cannot promise that it will be soon.

romantitov avatar Feb 04 '21 09:02 romantitov

@romantitov assuming you're able to detect the call to Include, I think there's still a problem. For instance, if I take @leopripos' code as an example:

var student1 = new Student(){
  Id = 1,
  Lessons = new List<Lesson>(){
    new Lesson(){
      Id = 1,
    }
  }
}

var students = new List<Student>(){
  student1
};

var dataSetMock = students.AsQueryable().BuildMockDbSet();

dataSetMock will return student1 as-is. But if you take the use of Include into account, dataSetMock without Include(s => s.Lessons) should return student1 without the content of Lessons, so you would need to either (1) clear the Lessons list on the original student1 instance, or (2) clone student1 without the list.

Both approaches have issues:

  • (1) would modify the original object, which the user might not expect. Also, you would need to keep track of the original content of Lessons somehow is case the query is repeated with the appropriate Include
  • (2) would return a different instance than the original. I think this is reasonable, but it's a breaking change compared to the current behavior, and might break some tests (I know it would break some of mine). Also, there's the problem of how to clone the original instance, which might not always be possible

thomaslevesque avatar Feb 04 '21 10:02 thomaslevesque

@thomaslevesque I see it a bit different. I think that it's not a good idea to create a new unexpected instance, but I would like to make it possible to verify that expected Include was executed:

var student1 = new Student(){
  Id = 1,
  Lessons = new List<Lesson>(){
    new Lesson(){
      Id = 1,
    }
  }
}

var students = new List<Student>(){
  student1
};

var dataSetMock = students.AsQueryable().BuildMockDbSet();
.....


dataSetMock.VerifyInclude(x => x.Lessons );

romantitov avatar Feb 05 '21 08:02 romantitov

Ah, I see

thomaslevesque avatar Feb 05 '21 11:02 thomaslevesque