Entity Framework, eager loading entites - c#

I have 3 classes which I would like to 'talk' to each other on a ASP.NET MVC C# WEBAPI app. They are, Item, which can have only one User but the User can make multiple Comments on multiple Items and a Comment can have multiple Users but only one Item
My classes are as follows:
public class Item
{
public int Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public ICollection<Comment> Comments { get; set; }
public User User { get; set; }
}
public class User
{
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
public ICollection<Item> Items { get; set; }
public ICollection<Comment> Comments { get; set; }
}
public class Comment
{
public int Id { get; set; }
public string Message { get; set; }
public bool Important { get; set; }
public Item Item { get; set; }
public User User { get; set; }
}
I'm using angularJs front end, and so that I don't get a forever repeating loop I have configured the following:
GlobalConfiguration.Configuration.Formatters.JsonFormatter.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Serialize;
GlobalConfiguration.Configuration.Formatters.JsonFormatter.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
I'm using entity framework 6 and I want to Display all items including the comments and the users who have commented
I have read and feel? that Projection using Linq is probably best?
I have the following in my dbContext. (P.S, I've disabled LazyLoading, and including the System.Data.Entity namespace)
using(var _db = new dbContext)
{
var model = _db.Items.Include(i=>i.Comments.Select(p=>p.User).Select(vm=>new ViewModelItem(){
//here I think is where I would say....
ViewModelItem.Name = x.Name,
ViewModelItem.Description = x.Description,
ViewModelItem.Comments = ///
ViewModelItem.Comments.User.Name = ///
})).ToList();
return Ok(model);
}
I'm not sure where to go from here.
So I want to display All the comments and include the User who owns the Item but also include All the comments for that Item, and all the Users who have commented on that Item.
Without causing an infinite loop.
If I'm not being clear, please ask me to clarify. Any help as always is greatly appreciated.
Thank you

Assuming your comments data is good this should do it.
var model = db.Comment.Select(p=>
new ViewModelItem {
Name = p.User.Name,
Comments=p,
Description=p.Item.Description,
});

Related

LINQ Projection and loading child objects

Having an issue with projection and getting child objects to load. The following is simplified code to represent the logic I'm trying to implement, not the actual code.
public class TicketItem
{
public int TicketItemId { get; set; }
public string TicketReason { get; set; }
public Station Station { get; set; }
public TicketOwner TicketOwner { get; set; }
}
public class Station
{
public int StationId { get; set; }
public string Name { get; set; }
}
public class TicketOwner
{
public int TicketOwnerId { get; set; }
public Employee Employee { get; set; }
public Organization Organization { get; set; }
}
public class Employee
{
public int EmployeeId { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
}
public class Organization
{
public int OrganizationId { get; set; }
public string Code { get; set; }
public string Name { get; set; }
}
public class CommonReasons
{
public int CommonReasonId { get; set; }
public string Reason { get; set; }
}
public TicketItem GetById(int id)
{
var query = from i in _dataContext.TicketItems
.Include("Station")
.Include("TicketOwner.Employee")
.Include("TicketOwner.Organization")
join r in _dataContext.CommonReasons on i.TicketReason equals r.CommonReasonId.ToString() into r1
from r2 in r1.DefaultIfEmpty()
where i.TicketItemId == id
select new TicketItem {
TicketItemId = i.TicketItemId,
TicketReason = r2.Reason == null ? i.Reason : r2.Reason,
Station = i.Station,
TicketOwner = i.TicketOwner
};
return query
.AsNoTracking()
.FirstOrDefault();
}
Most the code is self-explanatory. The part that is indirectly causing the trouble would be the relationship between TicketItem.TicketReason property (a string) and the CommonReasons entity. From the user interface side, the end-user has an input field of "Reason", and they can select from "common" reasons or input an adhoc reason. They original developer chose to have the TicketReason property contain either the key ID from the CommonReasons table (if the user selected from drop-down) or the adhoc reason typed in.
So, to handle this logic in the linq query, the only way I have found is to do a left join between TicketItem.TicketReason and CommonReasons.CommonReasonId, then use projection to modify the TicketReason column returning either the common reason text or adhoc text. If there is a different way to do this that would get me around the trouble I'm having with projection/include, I'm all ears.
For the "reason" logic, this query works, returning the proper text. The trouble is that none of the "grand-child" objects are returning, i.e. TicketItem.TicketOwner.Employee, TicketItem.TicketOwner.Organization. How do I get those objects to return also?
Changing the structure of the tables would be an absolute last resort, just based on the amount of code that would have to change. There are other spots in the code that are using the above logic but don't need the child objects.
Any help would be appreciated. Hope I've explained enough.

Converting infinitely nested objects in .NET Core

EDIT: I originally worded this question very poorly, stating the problem was with JSON serialization. The problem actually happens when I'm converting from my base classes to my returned models using my custom mappings. I apologize for the confusion. :(
I'm using .NET Core 1.1.0, EF Core 1.1.0. I'm querying an interest and want to get its category from my DB. EF is querying the DB properly, no problems there. The issue is that the returned category has a collection with one interest, which has one parent category, which has a collection with one interest, etc. When I attempt to convert this from the base class to my return model, I'm getting a stack overflow because it's attempting to convert the infinite loop of objects. The only way I can get around this is to set that collection to null before I serialize the category.
Interest/category is an example, but this is happening with ALL of the entities I query. Some of them get very messy with the loops to set the relevant properties to null, such as posts/comments.
What is the best way to address this? Right now I'm using custom mappings that I wrote to convert between base classes and the returned models, but I'm open to using any other tools that may be helpful. (I know my custom mappings are the reason for the stack overflow, but surely there must be a more graceful way of handling this than setting everything to null before projecting from base class to model.)
Classes:
public class InterestCategory
{
public long Id { get; set; }
public string Name { get; set; }
public ICollection<Interest> Interests { get; set; }
}
public class Interest
{
public long Id { get; set; }
public string Name { get; set; }
public long InterestCategoryId { get; set; }
public InterestCategory InterestCategory { get; set; }
}
Models:
public class InterestCategoryModel
{
public long Id { get; set; }
public string Name { get; set; }
public List<InterestModel> Interests { get; set; }
}
public class InterestModel
{
public long Id { get; set; }
public string Name { get; set; }
public InterestCategoryModel InterestCategory { get; set; }
public long? InterestCategoryId { get; set; }
}
Mapping functions:
public static InterestCategoryModel ToModel(this InterestCategory category)
{
var m = new InterestCategoryModel
{
Name = category.Name,
Description = category.Description
};
if (category.Interests != null)
m.Interests = category.Interests.Select(i => i.ToModel()).ToList();
return m;
}
public static InterestModel ToModel(this Interest interest)
{
var m = new InterestModel
{
Name = interest.Name,
Description = interest.Description
};
if (interest.InterestCategory != null)
m.InterestCategory = interest.InterestCategory.ToModel();
return m;
}
This is returned by the query. (Sorry, needed to censor some things.)
This is not .NET Core related! JSON.NET is doing the serialization.
To disable it globally, just add this during configuration in Startup
services.AddMvc()
.AddJsonOptions(options =>
{
options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
}));
edit:
Is it an option to remove the circular references form the model and have 2 distinct pair of models, depending on whether you want to show categories or interests?
public class InterestCategoryModel
{
public long Id { get; set; }
public string Name { get; set; }
public List<InterestModel> Interests { get; set; }
public class InterestModel
{
public long Id { get; set; }
public string Name { get; set; }
}
}
public class InterestModel
{
public long Id { get; set; }
public string Name { get; set; }
public InterestCategoryModel InterestCategory { get; set; }
public class InterestCategoryModel
{
public long Id { get; set; }
public string Name { get; set; }
}
}
Note that each of the models has a nested class for it's child objects, but they have their back references removed, so there would be no infinite reference during deserialization?

Project data with children and grandchildren entities

I'm trying to get data in a suitable format for an api
What I would like is
Place
--Rating1
---RatingImage1.1
---RatingImage1.2
---UserName
---UserId
--Rating2
---RatingImage2.1
---RatingImage2.2
---UserName
---UserId
In a nutshell im trying to fetch a place, with its ratings(and rating images), with the names of the users who did the rating given the googlePlaceId
Tried this but it goes and does some circular fetching where once it fetches the user it then fetches the user rating and the response becomes massive
context.Places
.Include(x => x.Ratings.Select(y => y.User))
.Include(x => x.Ratings.Select(c => c.RatingImages))
.Single(x => x.GooglePlaceId == googlePlaceId);
I think projection or linq joins must be the way, but i havent had any success yet.
here are my POCOS
Place Poco
public class Place
{
public Place()
{
Ratings = new List<Rating>();
Favourites = new List<Favourite>();
}
public int Id { get; set; }
public string Name { get; set; }
public string GooglePlaceId { get; set; }
public ICollection<Rating> Ratings { get; set; }
public ICollection<Favourite> Favourites { get; set; }
}
Rating POCO
public class Rating
{
public Rating()
{
RatingImages = new List<RatingImage>();
}
public int Id { get; set; }
public float RatingValue { get; set; }
public string RatingComment { get; set; }
public int PlaceId { get; set; }
public Place Place { get; set; }
public string UserId { get; set; }
public AspNetUser User { get; set; }
public ICollection<RatingImage> RatingImages { get; set; }
}
User POCO
public partial class AspNetUser
{
public string UserName { get; set; }
public string Id { get; set; }
// the rest of the fields are omitted
}
Although you've omitted the definition of AspNetUser, I'm guessing it has a navigation property back to Ratings. Is this required anywhere else in your application? It won't affect the structure of your database, and removing it would allow your projection to work exactly as you've got it here. You'd still be able to display all ratings by a single user using a separate query - you've got to optimise for your most common scenario though.

Adding to a many to many relationship table produces duplicate values in db

I have three tables zRequest, zFacility and a table for a many to many relationship between the two with just the id's of each. My aim is to add a request with many facilities. But whenever I add a new request with some facilities, it adds the selected facilities to zFacility as well as adding to the relationship table.
This is the relevant code in the controller:
foreach (var facility in Facilities)
{
var facName = facility.Replace(".", " ");
var facQry = from fac in db.zFacilities where fac.FacilityName == facName select fac;
var facList = facQry.ToList();
var item = new zFacility();
item.FacilityId = facList.FirstOrDefault().FacilityId;
item.FacilityName = facList.FirstOrDefault().FacilityName;
//db.zFacility.Attach(item);
zrequest.zFacility.Add(item);
}
zrequestRepository.InsertOrUpdate(zrequest);
zrequestRepository.Save();
I have done some research and tried attaching each facility to the database via the commented line but this gave me another error because another entity of the same type already has the same primary key value
This is the code from zRequestRepository:
public void InsertOrUpdate(zRequest zrequest)
{
if (zrequest.RequestId == default(int)) {
// New entity
context.zRequests.Add(zrequest);
} else {
// Existing entity
context.Entry(zrequest).State = System.Data.Entity.EntityState.Modified;
}
}
What can I do to fix this? Let me know if I need to provide more information...
Edit, providing the relevant models as requested.
zFacility:
using System;
using System.Collections.Generic;
public partial class zFacility
{
public zFacility()
{
this.zRequest = new HashSet<zRequest>();
this.zRoom = new HashSet<zRoom>();
}
public short FacilityId { get; set; }
public string FacilityName { get; set; }
public virtual ICollection<zRequest> zRequest { get; set; }
public virtual ICollection<zRoom> zRoom { get; set; }
}
zRequest:
using System;
using System.Collections.Generic;
public partial class zRequest
{
public zRequest()
{
this.zFacility = new HashSet<zFacility>();
this.zRoom = new HashSet<zRoom>();
}
public int RequestId { get; set; }
public string ModCode { get; set; }
public short StatusId { get; set; }
public int WeekId { get; set; }
public short DayId { get; set; }
public short PeriodId { get; set; }
public short SessionLength { get; set; }
public short Semester { get; set; }
public short RoundNo { get; set; }
public string SpecialRequirement { get; set; }
public short UserId { get; set; }
public virtual zDay zDay { get; set; }
public virtual zPeriod zPeriod { get; set; }
public virtual zRound zRound { get; set; }
public virtual zStatus zStatus { get; set; }
public virtual zWeek zWeek { get; set; }
public virtual ICollection<zFacility> zFacility { get; set; }
public virtual ICollection<zRoom> zRoom { get; set; }
public virtual zUser zUser { get; set; }
}
These were both generated via a database-first.
EF by default assume that the selected facilities (zFacility) are new and insert them into the database.
To avoid that , you will need to change the state to Unchanged for zFacility
Something like this
public void InsertOrUpdate(zRequest zrequest)
{
if (zrequest.RequestId == default(int)) {
context.zFacility.Attach(zrequest); // state Unchanged for zFacility
// New entity
context.zRequests.Add(zrequest);
} else {
// Existing entity
context.Entry(zrequest).State = System.Data.Entity.EntityState.Modified;
}
}
What you'll need to do is to select zFactories objects from DB instead of creating new object for each one of them because that makes EF think that you want to create completely new record.
So:
Select zFactories
Add them to the zRequest.zFacility (if they're not already there)
Save
It's a little hard to answer this question as there's some holes in your code. Based on usage, I'm assuming that Facilities is an enumerable of strings, and is created from the selected items in your post data.
I'm not sure why you're creating a new zFacility instead of using the one that you've already pulled from the database. Doing it the way you are, you're also introducing the possibility of null reference exceptions if there's no matching facility.
Anyways, I would change the code you have to:
// This is so you can run a single minimal query to select
// all the facilities you'll be working with
var facNames = Facilities.Select(m => m.Replace(".", " "));
var facList = db.zFacilities.Where(m => facNames.Contains(m.FacilityName)).ToList();
// Now add all facilities not currently attached
facList.Where(m => !zrequest.zFacility.Contains(m)).ToList()
.ForEach(m => zrequest.zFacility.Add(m));
You probably will also need to remove items that have been deselected. The easiest way to do that is to do the last bit of code in reverse:
zrequest.zFacility.Where(m => !facList.Contains(m)).ToList()
.ForEach(m => zrequest.zFacility.Remove(m));
EDIT
Actually, a more succinct way to remove deselected items would be:
zrequest.zFacility.RemoveAll(m => !factList.Contains(m));

EF5 and Lamba/Linq - how only Include some columns of related tables

I have a method in my repository to retrieve All records for Items
public IQueryable<Item> GetAll()
{
//The following causes a circular reference if you attempt to serialize it via an API call.
IQueryable<Item> items = context.Items.Include(c => c.UserProfile).Include(c => c.UserProfile1).AsQueryable();
return items;
}
This causes issues with Kendo Grid and serialization because of how I am including the foreign tables User Profile twice to be able to get the full name of the user whom created and modified the Item record.
Instead of Include(c => c.UserProfile) is there a way to only include the UserProfile.FullName column?
Today I am handling this in my ViewModel and creating a new subclass (this example is for Locations, not Items):
public class LocationsListViewModel
{
public IEnumerable<LocationsGrid> Locations { get; set; }
public IEnumerable<Facility> Facilities { get; set; }
public IEnumerable<string> AreaOptions { get; set; }
public int LocationCount { get; set; }
public class LocationsGrid
{
public int Id { get; set; }
public string DisplayLocation { get; set; }
public string Area { get; set; }
public string Zone { get; set; }
public string Aisle { get; set; }
public string Bay { get; set; }
public string Level { get; set; }
public string Position { get; set; }
public string Barcode { get; set; }
}
}
and then having to populate that in my Tasks or App Services layer (sits between controller and repository) like this:
viewModel.Locations = from l in locations.ToList()
select new LocationsListViewModel.LocationsGrid
{
Id = l.Id,
DisplayLocation = l.DisplayLocation,
Area = l.Area,
Zone = l.Zone,
Aisle = l.Aisle,
Bay = l.Bay,
Level = l.Level,
Position = l.Position,
Barcode = l.BarcodeValue
};
This seems like a lot of extra code and maintenance for each entity going forward. I'm sure there is a more efficient way to do this.
I typically use a Data-Transfer Object (basically just a class that has the exact data you're looking for, then returning objects of that type from your data-access method.
public IQueryable<ItemSummary> GetAll()
{
IQueryable<ItemSummary> items = context.Items
.Select(c => new ItemSummary {
FirstProfileName = c.UserProfile.FullName,
SecondProfileName = c.UserProfile1.FullName,
ScalarProp1 = c.ScalarProp1,
...
})
.AsQueryable();
return items;
}
I'm not sure if that will work the way you want it to, since I'm not familiar with Kendo Grid and such, but it may be useful.

Categories