When to initialize collection properties in Entities? - c#

I am confused about initializing navigation properties which are of a collection type. In examples I see on web, the properties are always explicitly initialized in class constructor, however in my project I have two sets of such properties and one work without such initialization, one not:
public class User
{
public int UserId { get; set; }
[System.ComponentModel.DataAnnotations.Schema.InverseProperty("Requested")]
public virtual System.Collections.Generic.ICollection<Friendship> RequestedFriendships { get; set; }
[System.ComponentModel.DataAnnotations.Schema.InverseProperty("Received")]
public virtual System.Collections.Generic.ICollection<Message> ReceivedMessages { get; set; }
}
public class Message
{
public int MessageId { get; set; }
string Message { get; set; }
public virtual System.Collections.Generic.ICollection<User> Received { get; set; }
}
public class Friendship
{
public int FriendshipId { get; set; }
public int RequestedUserId { get; set; }
public virtual User Requested { get; set; }
}
The following works:
db.Users.Single(u => u.UserId == userId).RequestedFriendships.Add(new B.Models.Friendship
{
RequestedUserId = userId,
});
Also this:
B.Models.Message message = db.Messages.Add(new B.Models.Message
{
Message = text
});
db.SaveChanges();
However following the above the below fails:
message.Received.Add(db.Users.Single(u => u.UserId == userId));
Since message.Received is null above. What is the difference here from the first successful one?

The difference i see here is that your User instance is loaded from the database, which means that EF sets your properties, including navigation collections (with empty collections if no relationship is found for this User). However, your Message instance is created in your code (new B.Models.Message { Message = text }), your navigation collection is not initialized so you get an exception when you try to add an item in it.
I suggest you always initialize such navigation collections in your constructors when dealing with entities.

Related

Entity framework core load navigation properties for recursive table

I am using Entity Framework Core 5.0 and the following model:
So a Job has a MainFlow, and the mainflow can have 0 or more Subflows which in turn can have subflows (recursive)
The way this has been setup is that I have a Job entity which has a MainflowId property (and also navigation property)
The Flows have a ParentFlowId property, and a navigation property with a collection of SubFlows.
I also included a TreeId property on Job and Flow to easily identify a tree.
public class Job
{
public int Id { get; set; }
public DateTimeOffset Timestamp {get; set; }
public string JobName{ get; set; }
public int MainFlowId { get; set; }
public Guid TreeId { get; set; }
public virtual Flow MainFlow { get; set; }
}
public class Flow
{
public int Id { get;set; }
public string Name { get; set; }
public string DisplayName { get; set; }
public Guid TreeId { get; set; }
public int? ParentFlowId { get; set; }
public virtual Flow ParentFlow { get; set; }
public virtual ICollection<Flow> SubFlows { get; set; } = new List<Flow>();
}
What I am trying to achieve is to load a list of jobs (for example based upon the timestamp), but in a way that all the navigation properties become available (Job->Mainflow->Subflow->Subflow->...)
I was able to get this behavior by loading the jobs, and then loading the flows separately using the TreeId, however because of performance issues I am now using .AsNoTracking() and this does not seem to work anymore.
Is there a way to load the whole trees with their navigation properties while using .AsNoTracking?
Thanks for any insight!
Edit:
Here is the way it is working without the AsNoTracking()
(I simplified the code a bit)
private IQueryable<Job> GetAllJobs()
{
return DbContext.Set<Job>()
.Include(a=>a.MainFlow)
}
private IEnumerable<Flow> GetAllFlowsForTreeIds(IEnumerable<Guid> treeIds)
{
var result = from flow in DbContext.Set<Flow>()
.Include(a => a.ParentFlow)
.AsEnumerable()
join treeId in treeIds
on flow.TreeId equals treeId
select flow;
return result;
}
public IEnumerable GetJobTrees()
{
var allJobs = GetAllJobs().ToList();
var flows = GetAllFlowsForTreeIds(allJobs.Select(a=>a.TreeId)).ToList());
//by getting the flows, the navigation properties in alljobs become available
}

Entity Framework .Include throwing NullReferenceException

I have a very basic EF setup that is throwing an odd error when trying to populate a navigation property by using .Include. Here are the entity Models:
public class LineGroup
{
public int ID { get; set; }
public string Name { get; set; }
public bool IsActive { get; set; }
public ICollection<LineGroupMember> LineGroupMembers { get; set; }
}
public class LineGroupMember
{
public int ID { get; set; }
public int Extension { get; set; }
public string Name { get; set; }
public int Permissions { get; set; }
public bool IsLoggedIn { get; set; }
public int LineGroupID { get; set; }
internal LineGroup LineGroup { get; set; }
}
I am using these through an injected DB context, and can query each just fine without using navigation properties. I can also query the LineGroups and include the LineGroupMembers property just fine, like so:
var LineGroups = _context.LineGroups.Include(l => l.LineGroupMembers).ToList();
This load all of the line groups into a list that has a correctly working "LineGroupMembers" collection for each Line Group. However, if I try
var lineGroupMembers = _context.LineGroupMembers.Include(m => m.LineGroup).ToList();
I get "NullReferenceException" with no helpful details. Any ideas why the navigation property will work one way and not the other? There are no null values in either database table...
Make your navigation property public
public LineGroup LineGroup { get; set; }
If it is internal it won't be picked up by default by EF. You could also add explicit fluent mapping to force EF to recognize it as well.

Relationships in MVC

I'm trying to access a variable that i have done true a relationship in my model:
// Threads
public class Thread
{
[Key]
public int Id { get; set; }
public string Name { get; set; }
public int NumberOfMessages { get; set; }
public int ViewCount { get; set; }
public string[] Tags { get; set; }
public DateTime LastEdited { get; set; }
public int Vote { get; set; }
// Other model relations
public virtual ICollection<Message> Messages { get; set; }
public virtual Group Group { get; set; }
public virtual ForumUser User { get; set; }
}
I'm trying to add a message to the database and also reference it in the list of messages
thread.Group.Id = groupId;
It gave me a NULL reference exception. I do really now know how this works.
I'm going to show a other example that might explain a little bit more
` // Saving the message in the database
forumDb.Messages.Add(block);
forumDb.Threads.Find(threadId).Messages.Add(block);
forumDb.SaveChanges();
}`
It would be nice if some one example what I'm doing wrong the model for that last example is the same
From the information you gave, it looks like you didn't initialize the Group property of the Thread.
When creating an object, it's properties are not initialized. Since you try to access the id property of an uninitialized Group object, you get a null reference exception.
To solve your problem you could do this:
thread.Group = new Group(); // First initialize the group
thread.Group.Id = groupId; // Set the id of the newly created Group object.

While Including Primary Keys as a property of my classes in C# using ASP.NET MVC5

If I supply a primary key property in my classes does Entity Framework implicitly iterate the value of newly created objects or do I have to supply code to register this information? This may seem obvious to some, and to be fair I researched this question to no avail before asking it.
Reason I need to know:
I need to retrieve a SelectList of items from a class, of which is a property of another class. The classes share a composition relationship in other words.
// GET: Fora/Create
public ActionResult Create()
{
ViewBag.Categories = new SelectList(db.Fora.OrderBy(m => m.MessageItem.TopicItem.Category), "TopicID", "Category");
ViewBag.UserNames = new SelectList(db.Fora.OrderBy(m => m.MessageItem.From.UserName), "MemberID", "UserName");
return View();
}
Classes:
public class ForumViewModel
{
public int MessageID { get; set; }
public Member User{ get; set; }
public Topic Category { get; set; }
public Message Message { get; set; }
}
public class Message
{
private List<Topic> categories = new List<Topic>();
public virtual int MessageID { get; set; }
public virtual int MemberID { get; set; }
public virtual int ForumID { get; set; }
public virtual int TopicID { get; set; }
public virtual string Subject { get; set; }
public virtual string Body { get; set; }
public virtual DateTime Date { get; set; }
public virtual Member From { get; set; }
public virtual Topic TopicItem { get; set; }
public virtual List<Topic> Categories
{
get { return categories; }
set { categories = value; }
}
}
Before anyone rips me apart for the class structure, I would just like to say I am a college student still trying to learn this stuff and out of desperation to get the action create() method of my controller to work I have hacked at this class trying to force it to work, also to no avail...
Any help or clarification is appreciated.
If what you mean by "implicitly iterate the value of newly created objects" and "register this information" is actually adding the new objects to the database and getting EF updated about these, you need to take the steps to first create a new object, add it to the dbcontext and then save the changes. Assuming the new object is of type Message,
first create a new Message:
var newMessage = new Message{ ... };
then add this to your dbcontext:
db.Message.Add(newMessage);
(where Message is the dbContext class that represents your Message table)
and finally save your changes:
db.SaveChanges();
The new newMessage object will get a new id after saving your changes if your db is configured to auto-increment the primary key value of your Message table.
Hope this helps.

The number of elements in ICollection is zero in many-to-many relationship

I have a database made with Entity Framework. I have two tables Users and Advertisments and the relationship between them is Many-to-Many. Everything is working fine except when I want to return the number of ICollection in class Users.
[Table("Advertisments")]
public class Advertisment
{
public Advertisment()
{
Users = new HashSet<User>();
}
[Key]
public int AdvertismentID { get; set; }
public string Address { get; set; }
public string City { get; set; }
public double Rating { get; set; }
public int NumberOfRates { get; set; }
public string Title { get; set; }
public string PhoneNumber { get; set; }
public string Email { get; set; }
public double Latitude { get; set; }
public double Longitude { get; set; }
public ICollection<User> Users { get; set; }
}
[Table("Users")]
public class User
{
public User()
{
FavouriteAdvertisments = new HashSet<Advertisment>();
}
[Key]
public int UserID { get; set; }
public string Username { get; set; }
public string Password { get; set; }
public string Name { get; set; }
public string Surname { get; set; }
public ICollection<Advertisment> FavouriteAdvertisments { get; set; }
}
public class GetHiredDBContext : DbContext
{
public GetHiredDBContext()
: base("GetHiredDBContext")
{ }
public DbSet<User> Users { get; set; }
public DbSet<Advertisment> Advertisments { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<User>().HasMany(a => a.FavouriteAdvertisments).WithMany(u => u.Users).Map(m =>
{
m.MapLeftKey("UserID");
m.MapRightKey("AdvertismentID");
m.ToTable("UserAdvertisment");
});
}
}
And this is what I want to do:
public ICollection<Advertisment> favouriteAdvertismentsByUser(int UserID)
{
GetHiredDBContext db = new GetHiredDBContext();
foreach (User user in db.Users)
{
if (user.UserID == UserID)
{
return user.FavouriteAdvertisments;
}
}
return null;
}
Everytime I call this method, the number of elements in ICollection for every user is 0!
public ICollection<Advertisment> favouriteAdvertismentsByUser(int UserID)
{
GetHiredDBContext db = new GetHiredDBContext();
// First of all, you probably forgot to "include" FavouriteAdvertisments
var users = db.Users.Include(u => u.FavouriteAdvertisments);
// Second of all, use linq!
return users.SingleOrDefault(u => u.UserID == UserID).FavouriteAdvertisments;
}
If your using Entity Framework you need to write your queries in Linq so the query provider can translate that into a SQL statement. As you have it now it is doing a table scan. Instead try this:
public ICollection<Advertisment> favouriteAdvertismentsByUser(int UserID)
{
return new GetHiredDbContext()
.Users
.Single(u => u.UserID = UserID)
.FavouriteAdvertisements;
}
One thing to note, this method now expects there to be exactly 1 record in your table with that UserID. It will throw an exception if it does not exist. Personally I prefer this because if I'm calling a method I expect it to work, and exception would mean I coded something wrong allowing me to find bugs earlier. You also do not have to check if your collection is null before getting the count.
The way your entites are currently set up, you will have to either use Eager, or Explicit loading, your related entites will not be loaded automatically.
To Explicitly load, I believe you can use your original query (provided you're passing an entity that can be found in the DBSet, and make an explicit call to load the related information (using Load):
E.g. (provided your entity can be found).
public ICollection<Advertisment> favouriteAdvertismentsByUser(User userEntity)
{
// Load the blog related to a given post
GetHiredDBContext db = new GetHiredDBContext();
db.Entry(userEntity).Reference(p => p.FavouriteAdvertisments).Load();
return user.FavouriteAdvertisments;
}
Although it's probably cleaner to obtain your entity from your context, and call load on that, rather than interating through your entire set.
To Eagerly load, you make your load request at the time of query, using Include:
public ICollection<Advertisment> favouriteAdvertismentsByUser(int userID)
{
GetHiredDBContext db = new GetHiredDBContext();
User myUser = db.Users
.Where(x => x.UserID = userID)
.Include(x => x.FavouriteAdvertisments)
.FirstOrDefault();
return myUser.FavouriteAdvertisments;
}
To obtain the data the third way, using Lazy-Loading, you would have to make some alterations to your classes, namely marking your ICollection navigation properties as virtual, so entity framework is able to create valid proxy types for your classes. Your data would be available as required, and loaded on demand.
Hopefully I haven't got the problem completely wrong, just about to shut down/sleep.
Good luck.
More info: http://msdn.microsoft.com/en-us/data/jj574232.aspx

Categories