r3plica
r3plica

Reputation: 13367

EF Lazy Loading

I am a little confused with Lazy Loading. I was under the impression that it loads navigational properties when they are accessed, but in my code it seems to try to pull it all in. This could be due to my Service/Repository pattern, but at the moment I am getting circular references :(

When I called my service like this:

using (var service = new UserService(new CompanyService()))
{
    var u = await service.GetAll();                  

    return new JsonResult { Data = new { success = true, users = u } }; // Return our users
}

it is bringing in a list of User

public partial class User : IdentityUser
{
    public User()
    {
        // ...
        this.MemberOf = new List<Group>();
        // ...
    }

    // ...

    // ...
    public virtual ICollection<Group> MemberOf { get; set; }
    // ...
}

Which then seems to bring in a list of Group

public partial class Group
{
    public Group()
    {
        // ...
    }

    public int Id { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }
    // ...

    // ...
    public virtual Company Company { get; set; }
    // ...
}

which then brings in a Company

public partial class Company
{
    public Company()
    {
        this.Assets = new List<Asset>();
        // ..
    }

    public string Id { get; set; }
    public string Name { get; set; }
    // ..
            
    // ...
    public virtual ICollection<Asset> Assets { get; set; }
    // ...
}

which brings in a list of Asset

public partial class Asset
{
    public Asset()
    {
        // ...
        this.Categories = new List<Category>();
        // ...
    }

    public int Id { get; set; }
    public string FileName { get; set; }
    public string ThumbNail { get; set; }
    // ...

    // ...
    public virtual ICollection<Category> Categories { get; set; }
    // ...
}

which brings in a list of Category and this is where the circular reference happens because it is bringing in a list of Asset which is bring in a list of Category and so on.

I thought that using Lazy loading, it would only bring in the Users and their navigational properties and that is it, unless I tell it otherwise?

I tried just using this method instead of my service (just to test);

var u = new SkipstoneContext().Users;

return new JsonResult { Data = new { success = true, users = u } }; // Return our users

but I still get the circular reference.

Can someone explain to me why it is trying to load all navigational properties and if there is something I can (easily) do to stop it?

Update 2

Keith suggested using Interfaces and a ContractResolver to help with the serialisation, so this is what I did.

First, I created 2 new interfaces:

public interface IBaseUser
{
    string Id { get; set; }
    string UserName { get; set; }
    string Email { get; set; }
    bool IsApproved { get; set; }
    bool IsLockedOut { get; set; }

    ICollection<Group> MemberOf { get; set; }
}

and

public interface IBaseGroup
{
    int Id { get; set; }
    string Name { get; set; }
}

and the Models implemented these classes

public partial class User : IdentityUser, IBaseUser

and

public partial class Group : IBaseGroup

so that was the first step, the second step was to create the ContractResolver class which looks like this:

public class InterfaceContractResolver : DefaultContractResolver
{
    private readonly Type interfaceType;

    public InterfaceContractResolver(Type interfaceType)
    {
        this.interfaceType = interfaceType;
    }

    protected override IList<JsonProperty> CreateProperties(Type type, MemberSerialization memberSerialization)
    {
        var properties = base.CreateProperties(this.interfaceType, memberSerialization);

        return properties;
    }
}

and my method for getting the users now looks like this:

public async Task<JsonNetResult> Get()
{
    try
    {
        using (var service = new UserService(new CompanyService()))
        {
            var u = await service.GetAll();

            var serializedObject = JsonConvert.SerializeObject(u,
                new JsonSerializerSettings()
                {
                    ContractResolver = new InterfaceContractResolver(typeof(IBaseUser))
                });

            return new JsonNetResult { Data = new { success = true, users = serializedObject } }; // Return our users
        }
    }
    catch (Exception ex)
    {
        return new JsonNetResult { Data = new { success = false, error = ex.Message } };
    }
}

So, when this code runs I get an error:

Unable to cast object of type 'System.Data.Entity.DynamicProxies.Group_D0E52FCCF207A8F550FE47938CA59DEC7F963E8080A64F04D2D4E5BF1D61BA0B' to type 'Skipstone.Web.Identity.IBaseUser'.

which makes sense because the InterfaceContractResolver only expects on interface type.

The question is how would I get around that?

Update 3

Ok, this is getting silly now.

So I disabled Lazy Loading by doing this:

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        base.Configuration.LazyLoadingEnabled = false; // Disable Lazy Loading

        // ...
    }

And then I created this repository:

public abstract class Repository<TEntity> : IDisposable, IRepository<TEntity> where TEntity : class
{
    public DbContext Context { get; private set; }
    public IQueryable<TEntity> EntitySet { get { return this.DbEntitySet; } }
    public DbSet<TEntity> DbEntitySet { get; private set; }

    public Repository(DbContext context)
    {
        if (context == null)
            throw new ArgumentNullException("context");

        this.Context = context;
        this.DbEntitySet = context.Set<TEntity>();
    }

    public Task<TEntity> GetAsync(object id)
    {
        return this.DbEntitySet.FindAsync(new object[]
        {
            id
        });
    }

    public IEnumerable<TEntity> GetAll()
    {
        return this.DbEntitySet;
    }

    public IEnumerable<TEntity> GetAll(string include)
    {
        return this.DbEntitySet.Include(include);
    }

    public IEnumerable<TEntity> GetAll(string[] includes)
    {
        foreach (var include in includes)
            this.DbEntitySet.Include(include);

        //var t = this.DbEntitySet.Include("Settings").ToList();

        // return this.GetAll();
        return this.DbEntitySet;
    }

    public void Add(TEntity model)
    {
        this.DbEntitySet.Add(model);
    }

    public void Remove(TEntity model)
    {
        this.Context.Entry<TEntity>(model).State = EntityState.Deleted;
    }

    public void Dispose()
    {
        this.Context.Dispose();
    }
}

which my UserRepository inherits:

public class UserRepository : Repository<User>
{
    public UserRepository(DbContext context)
        : base(context)
    {
    }
}

Then my service has this method:

    public IList<User> GetAll(string include)
    {
        return this.repository.GetAll(include).Where(model => model.CompanyId.Equals(this.companyId, StringComparison.OrdinalIgnoreCase)).ToList();
    }

and now my JsonResult method looks like this:

    // 
    // AJAX: /Users/Get

    public JsonResult Get()
    {
        try
        {
            using (var service = new UserService(new CompanyService()))
            {
                var u = service.GetAll("MemberOf");                  

                return new JsonResult { Data = new { success = true, users = u } }; // Return our users
            }
        }
        catch (Exception ex)
        {
            return new JsonResult { Data = new { success = false, error = ex.Message } };
        }
    }

and guess what?!?!? If I put a breakpoint just before the return I see that u has it's properties all populated and the navigation properties are all not set except MemberOf which is exactly what I want (for now) but when I step over the return I get the same error as before!!!!

An exception of type 'Newtonsoft.Json.JsonSerializationException' occurred in Newtonsoft.Json.dll but was not handled in user code

Additional information: Self referencing loop detected with type 'System.Data.Entity.DynamicProxies.Asset_AED71699FAD007BF6F823A5F022DB9888F62EBBD9E422BBB11D7A191CD784288'. Path 'users[0].Company.Assets[0].Categories[0].Assets'.

How/Why is this possible?

Update 4

This appears to because the properties were still marked with Virtual. When I removed virtual I stopped getting the error for the Assets, instead I now get it for the CreatedBy property.

This is my User class:

public partial class User : IdentityUser
{
    public string CompanyId { get; set; }
    public string CreatedById { get; set; }
    public string ModifiedById { get; set; }
    public System.DateTime DateCreated { get; set; }
    public Nullable<System.DateTime> DateModified { get; set; }
    public System.DateTime LastLoginDate { get; set; }
    public string Title { get; set; }
    public string Forename { get; set; }
    public string Surname { get; set; }
    public string Email { get; set; }
    public string JobTitle { get; set; }
    public string Telephone { get; set; }
    public string Mobile { get; set; }
    public string Photo { get; set; }
    public string LinkedIn { get; set; }
    public string Twitter { get; set; }
    public string Facebook { get; set; }
    public string Google { get; set; }
    public string Bio { get; set; }
    public string CompanyName { get; set; }
    public string CredentialId { get; set; }
    public bool IsLockedOut { get; set; }
    public bool IsApproved { get; set; }
    public bool CanEditOwn { get; set; }
    public bool CanEdit { get; set; }
    public bool CanDownload { get; set; }
    public bool RequiresApproval { get; set; }
    public bool CanApprove { get; set; }
    public bool CanSync { get; set; }
    public bool AgreedTerms { get; set; }
    public bool Deleted { get; set; }

    public Company Company { get; set; }
    public User CreatedBy { get; set; }
    public User ModifiedBy { get; set; }
    public ICollection<Asset> Assets { get; set; }
    public ICollection<Category> Categories { get; set; }
    public ICollection<Collection> Collections { get; set; }
    public ICollection<Comment> Comments { get; set; }
    public ICollection<LocalIntegration> LocalIntegrations { get; set; }
    public ICollection<Page> Pages { get; set; }
    public ICollection<Rating> Ratings { get; set; }
    public ICollection<Theme> Themes { get; set; }
    public ICollection<Group> MemberOf { get; set; }
    public ICollection<Category> ForbiddenCategories { get; set; }
    public ICollection<Page> ForbiddenPages { get; set; }
}

Here is the error:

An exception of type 'Newtonsoft.Json.JsonSerializationException' occurred in Newtonsoft.Json.dll but was not handled in user code

Additional information: Self referencing loop detected for property 'CreatedBy' with type 'System.Data.Entity.DynamicProxies.User_E9B58CAAA82234358C2DE2AF8788D33803C4440F800EA8E015BE49C58B010EDF'. Path 'users[0]'.

I don't understand HOW the CreatedBy property is being realised because it is not a virtual property and I do not ask for it using Eager or Explicit loading......

Does anyone know why?

Upvotes: 2

Views: 1242

Answers (2)

ken2k
ken2k

Reputation: 48965

The JSON serialization is achieved internally by using reflection. So it tries to access every public property you have to build the JSON result.

You can add the [JsonIgnore] attribute on navigation properties you don't want to be serialized. This could also be useful to prevent the serialization to produce circular references (i.e. for many to many relationship for example).

That being said, I would recommend to use ViewModels instead of the entities. Sooner or later you'll need to add other properties than the properties directly coming from the entity itself (for example: read only computed field, other field not related to your entity but required in your Form...etc.). You could add those properties in a partial definition of the entity, but when you'll have many views it won't be maintainable/readable anymore.

Second point is, unless you really need it for a specific reason, I would recommend deactivating lazy-loading for three main reasons:

  • you could have serious performance issues if you're not very familiar with your ORM (you could google for "extra lazy loading" for instance)
  • extremely hard to handle exceptions, as every part of your code might throw an exception related to a failure in the SQL request execution, not just the part that queries entities from the database context itself.
  • lazy loading tends to lengthen the context lifetime (as you can't dispose it until you're sure you'll never lazy load a property)

Upvotes: 1

Keith Payne
Keith Payne

Reputation: 3082

This might work:

Create one or more interfaces that define the properties that you want to serialize. One interface per "snapshot" of the entity.

Have your EF entity implement all of the interfaces.

Then create a contract resolver that works only on the properties that are defined on an interface type that is passed to the resolver. See Serialize only interface properties to JSON with Json.net and use the answer with contract resolver code.

Once you have the contract resolver coded, you can pass the EF entity and whichever interface you like.

Hopefully the serializer will ignore the properties that the contract resolver does not "see".

If this works, please post your own answer with your working code and mark it as the answer.

Upvotes: 0

Related Questions