Why is my code doing lazy loading even after I tur

2020-07-06 08:03发布

I would like to get Exams and Test entities that have a UserTest entity with a UserId that is either equal to "0" or to a provided value. I had a number of suggestions but so far none have worked. One suggestion was to start by getting UserTest data and the other solution was to start by getting Exam data. Here's what I have when I used the UserTests as the source starting point.

I have the following LINQ:

        var userTests = _uow.UserTests
            .GetAll()
            .Include(t => t.Test)
            .Include(t => t.Test.Exam)
            .Where(t => t.UserId == "0" || t.UserId == userId)
            .ToList();

When I check _uow.UserTests with the debugger it's a repository and when I check the dbcontext's configuration.lazyloading then it is set to false.

Here's my classes:

public class Exam
{
    public int ExamId { get; set; }
    public int SubjectId { get; set; }
    public string Name { get; set; }
    public virtual ICollection<Test> Tests { get; set; }
}

public class Test
{
    public int TestId { get; set; }
    public int ExamId { get; set; }
    public string Title { get; set; }
    public virtual ICollection<UserTest> UserTests { get; set; }
}

public class UserTest
{
    public int UserTestId { get; set; }
    public string UserId { get; set; }
    public int TestId { get; set; }
    public int QuestionsCount { get; set; }
}

When I looked at the output I saw something like this:

[{"userTestId":2,
  "userId":"0",
  "testId":12,
  "test":{
      "testId":12,"examId":1,
      "exam":{
          "examId":1,"subjectId":1,
          "tests":[
               {"testId":13,"examId":1,"title":"Sample Test1",
                "userTests":[
                      {"userTestId":3,
                       "userId":"0",

Note that it gets a UserTest object, then gets a test object and then an exam object. However the exam object contains a test collection and then it heads back down again and gets the different tests and the unit tests inside of those:

UserTest > Test > Exam > Test > UserTest ?

I have tried hard to ensure lazy loading is off and debug tell me it's set to false. I am using EF6 and WebAPI but not sure if that makes a difference as I am debugging at the C# level.

6条回答
老娘就宠你
2楼-- · 2020-07-06 08:17

If you try something like:

var userTest = _uow.UserTests
            .GetAll()
            .Where(t => t.UserId == "0" || t.UserId == userId)
            .First();

var name = userTest.Test.Title;

Would your code throw an exception because the Test property hasn't been loaded? I suspect the problem is your repository is using LINQ to SQL and not LINQ to Entities. You can't turn off Lazy Loading with LINQ to SQL. You would have to show how your repository works to fix the problem in that case.

查看更多
3楼-- · 2020-07-06 08:24

Your query will load all UserTests into the context where UserId == "0" || UserId == userId and you have eagerly loaded the related Test and its related Exams.

Now in the debugger you can see that the Exams are linked to some Tests in memory and are assuming that is because they have been lazy-loaded. Not true. They are in memory because you loaded all UserTests into the context where UserId == "0" || UserId == userId and you have eagerly loaded the related Test. And they are linked to the navigation property because EF performs a "fix-up" based on foreign keys.

The Exam.Tests navigation property will contain any entities loaded into the context with the correct foreign key, but will not necessarily contain all Tests linked to the Exam in the database unless you eagerly load it or turn on lazy loading

查看更多
在下西门庆
4楼-- · 2020-07-06 08:24

I believe that deferred execution causes nothing to happen unless something is actually read from userTests. Try to include var userTestsAsList = userTests.ToList() and check with the debugger if userTestsAsList contains the desired sequence.

查看更多
欢心
5楼-- · 2020-07-06 08:29

You can't avoid that the inverse navigation properties are populated by EF, no matter if you load related entities with eager or lazy loading. This relationship fixup (as already explained by @Colin) is a feature you can't turn off.

You could solve the problem by nullifying the unwished inverse navigation properties after the query is finished:

foreach (var userTest in userTests)
{
    if (userTest.Test != null)
    {
        userTest.Test.UserTests = null;
        if (userTest.Test.Exam != null)
        {
            userTest.Test.Exam.Tests = null;
        }
    }
}

However, in my opinion the flaw of your design is that you try to serialize entities instead of data transfer objects ("DTOs") that are specialized to the view where you want to send the data to. By using DTOs you can avoid the inverse navigation properties that you don't want altogether and maybe other entity properties that you don't need in your view. You would define three DTO classes, for example:

public class ExamDTO
{
    public int ExamId { get; set; }
    public int SubjectId { get; set; }
    public string Name { get; set; }
    // no Tests collection here
}

public class TestDTO
{
    public int TestId { get; set; }
    public string Title { get; set; }
    // no UserTests collection here

    public ExamDTO Exam { get; set; }
}

public class UserTestDTO
{
    public int UserTestId { get; set; }
    public string UserId { get; set; }
    public int QuestionsCount { get; set; }

    public TestDTO Test { get; set; }
}

And then use a projection to load the data:

var userTests = _uow.UserTests
    .GetAll()
    .Where(ut => ut.UserId == "0" || ut.UserId == userId)
    .Select(ut => new UserTestDTO
    {
        UserTestId = ut.UserTestId,
        UserId = ut.UserId,
        QuestionsCount = ut.QuestionsCount,
        Test = new TestDTO
        {
            TestId = ut.Test.TestId,
            Title = ut.Test.Title,
            Exam = new ExamDTO
            {
                ExamId = ut.Test.Exam.ExamId,
                SubjectId = ut.Test.Exam.SubjectId,
                Name = ut.Test.Exam.Name
            }
        }
    })
    .ToList();

You could also "flatten" the object graph by defining only a single DTO class that contains all the properties you need for the view:

public class UserTestDTO
{
    public int UserTestId { get; set; }
    public string UserId { get; set; }
    public int QuestionsCount { get; set; }

    public int TestId { get; set; }
    public string TestTitle { get; set; }

    public int ExamId { get; set; }
    public int ExamSubjectId { get; set; }
    public string ExamName { get; set; }
}

The projection would become simpler and look like this:

var userTests = _uow.UserTests
    .GetAll()
    .Where(ut => ut.UserId == "0" || ut.UserId == userId)
    .Select(ut => new UserTestDTO
    {
        UserTestId = ut.UserTestId,
        UserId = ut.UserId,
        QuestionsCount = ut.QuestionsCount,
        TestId = ut.Test.TestId,
        TestTitle = ut.Test.Title,
        ExamId = ut.Test.Exam.ExamId,
        ExamSubjectId = ut.Test.Exam.SubjectId,
        ExamName = ut.Test.Exam.Name
    })
    .ToList();

By using DTOs you do not only avoid the problems of inverse navigation properties but also follow good security practices to "white-list" the exposed property values from your database explicitly. Imagine you would add a test access Password property to the Test entity. With your code that serializes eagerly loaded full entities with all properties the password would get serialized as well and run over the wire. You don't have to change any code for this to happen and in the worst case you wouldn't be aware that you are exposing passwords in a HTTP request. On the other hand when you are defining DTOs a new entity property would only be serialized with your Json data if you add this property explicitly to the DTO class.

查看更多
够拽才男人
6楼-- · 2020-07-06 08:40

Is there any reson you are using "virtual" for your collections? If you're using "include", I would recommend getting rid of the "virtual"

查看更多
不美不萌又怎样
7楼-- · 2020-07-06 08:41

As far as I can read your POCO-Relationships and your query, your Repo is returning what you asked for. But did you know you asked for this?

You are navigating from Grand-Child <UserTest> to Child <Test> to Parent <Exam>

Your Entity of <Exam> is being treated as a Grand-Child when it seems to be a Grand-Parent (in fact, a graph root) having children of <Test> who have children / grand-children of type <UserTest>.

As you are eager loading (and serializing?), of course your <Exam> should eager load its <Test> Collection, which should load their <UserTest> Collections.

By working your way down the graph, you are causing a full circle.


Did you mean to have the opposite relationships?

public class Exam
{
    public int ExamId { get; set; }
    public int TestId { get; set; }
    public int SubjectId { get; set; }
    public string Name { get; set; }
}

public class Test
{
    public int TestId { get; set; }
    public int ExamId { get; set; }
    public string Title { get; set; }
    public virtual ICollection<UserTest> UserTests { get; set; }
}

public class UserTest
{
    public int UserTestId { get; set; }
    public string UserId { get; set; }
    public int TestId { get; set; }
    public int QuestionsCount { get; set; }
    public virtual ICollection<Exam> Exams { get; set; }
}

I'm making many assumptions about your data. This relationships simply makes more sense as real-world entities and by your usage. That Users have Tests and Exams rather than the reverse. If so, this relationship should work with your linq query.

查看更多
登录 后发表回答