Reputation: 13367
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
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:
Upvotes: 1
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