Lazy / Deferred loading of links not on time?

2019-04-15 17:37发布

问题:

Did someone experience the following? Validating objects with fields that refer to other Entities would throw you an error stating that the field wasn't present and that when you would debug the program and you would inspect the entities that the fields are populated.

This has happened to me on two occasions now and it seems to be some problem with lazy loading, as if the lazy loading did not give the answer fast enough.

We have this (simplified) model where

class Survey {
  ...
  public bool Enabled {get; set;}
  [Required]
  public virtual OrganisationalUnit OU {get; set;}
  ...
}

If we would just do Context.Surveys.Single(id) or Context.Surveys.Where(s => s.Id == id), changing the Enabled field (or any other field), and do a Context.SaveChanges() it would in 9 out of 10 times throw a validation error that the OU field is required and that it's not present.

After adding .Include(s => s.OU) this problem was solved and I thought this was the end of it. Although yesterday again I encountered a similar problem with the following code:

public class SurveyQuestionMultipleChoiceMultiSelect : SurveyQuestionMultipleChoice
{
    public override IEnumerable<ValidationResult> validateValue(string _, IEnumerable<string> values)
    {
        int ivalue;
        foreach( string value in values) {

            bool success = int.TryParse(value, out ivalue);

            if (!success || !Questions.Any(q => q.Id == ivalue))
                yield return new ValidationResult(String.Format(GUI.error_multiplechoice_answer_not_element_of, ivalue));
        }
    }
}

This would return ValidationErrors for values [4,5] whilst Questions upon inspection through the debugger indeed contained questions with Ids 4 and 5. If I would pause the debugger a moment on the if-statement the validation would go through correctly afterwards.

The odd thing is that I didn't (knowingly) experience these errors before and that I didn't update any libraries or database software.

This situation frightens me a bit as it seems I cannot rely on Lazy Loading to always work. Or maybe I'm doing something wrong?

This feels loosely related to EF 4.1 loading filtered child collections not working for many-to-many but I cannot explain how this would apply here.

Update1: The following exception would popup by following the steps provided in the first example:

System.Data.Entity.Validation.DbEntityValidationException was unhandled by user code
  Message=Validation failed for one or more entities. See 'EntityValidationErrors' property for more details.
  Source=EntityFramework
  StackTrace:
       at System.Data.Entity.Internal.InternalContext.SaveChanges()
       at System.Data.Entity.Internal.LazyInternalContext.SaveChanges()
       at System.Data.Entity.DbContext.SaveChanges()
       at Caracal.Application.Controllers.SurveyController.BulkEnable(SurveyBulkAction data) in C:\Users\Alessandro\Caracal\DigEvalProject\trunk\Caracal\application\Controllers\SurveyController.cs:line 353
       at lambda_method(Closure , ControllerBase , Object[] )
       at System.Web.Mvc.ActionMethodDispatcher.Execute(ControllerBase controller, Object[] parameters)
       at System.Web.Mvc.ReflectedActionDescriptor.Execute(ControllerContext controllerContext, IDictionary`2 parameters)
       at System.Web.Mvc.ControllerActionInvoker.InvokeActionMethod(ControllerContext controllerContext, ActionDescriptor actionDescriptor, IDictionary`2 parameters)
       at System.Web.Mvc.ControllerActionInvoker.<>c__DisplayClass15.<InvokeActionMethodWithFilters>b__12()
       at System.Web.Mvc.ControllerActionInvoker.InvokeActionMethodFilter(IActionFilter filter, ActionExecutingContext preContext, Func`1 continuation)
  InnerException: 
    <really-empty>

The code to achieve this (not written by me personally, but another team-member):

        bool option = data.option == "true";

        // Check if all surveys can be set to the enabled state
        foreach (int id in data.surveys)
        {
            Survey survey = Context.Surveys.SingleOrDefault(s => s.Id == id);
            if (survey == null || !survey.CanAdministrate(Context))
                return JsonResponse.Error(GUI.survey_enable_change_bulk_failed);

            surveys.Add(survey);
        }

        // Enable/disable the selected surveys.
        foreach (Survey survey in surveys)
            survey.Enabled = option;

        Context.SaveChanges();

data is an object containing the post-data from the client. survey.CanAdministrate(Context) uses the Context to read the whole tree of OrganisationalUnits from the DB to determine roles.

回答1:

This is by design and IMHO it is very good feature. Context internally turns off lazy loading in some operations and validation is one of them. This is part of internal implementation of the method which causes it:

public virtual DbEntityValidationResult GetValidationResult(IDictionary<object, object> items)
{
    EntityValidator entityValidator = 
        this.InternalContext.ValidationProvider.GetEntityValidator(this);
    bool lazyLoadingEnabled = this.InternalContext.LazyLoadingEnabled;
    this.InternalContext.LazyLoadingEnabled = false;
    DbEntityValidationResult result = null;
    try
    {
        ...
    }
    finally
    {
        this.InternalContext.LazyLoadingEnabled = lazyLoadingEnabled;
    }
    return result;
}

Why is it good? Because it avoids leaky lazy loads in case when you don't want them. Btw. if you placed validation logic on property which doesn't have to be loaded you did it wrong. It is your responsibility to ensure that all necessary properties are filled prior to validation.