Entity Framework proxycreation misery - c#

ive seen quite a few solutions on problems which are always similar to mine. But none of them satisfied my needs specifically.
I have the following setup:
public class User
{
public Int32 Id { get; set; }
public String Name { get; set; }
public virtual List<PhoneBook> Phonebooks { get; set; }
}
public class PhoneBook
{
public Int32 Id { get; set; }
public virtual Int32 UserId { get; set; }
public virtual User User { get; set; }
public String Title { get; set; }
public virtual List<PhoneNumber> Number { get; set; }
}
public class PhoneNumber
{
public Int32 Id { get; set; }
public String Num { get; set; }
public virtual Int32 PhoneBookId { get; set; }
public virtual PhoneBook PhoneBook { get; set; }
}
Now I can query perfectly fine. The problem is the circular referencing.
Let's say I would want to select every Phonebook thats for the user with the id = 1
My query would be
context.phonebook.Where(x=>x.UserId == 1)
This would load the user and every phonenumber as well.
I could turn off the proxying and I would get nothing but the Phonebook ( no numbers, no user ).
What if I wanted to get every phonenumber too but not the user?
Also I'd have to make sure that getting the phonenumber objects doesnt also mean it's getting the phonebook related to the number too.
I kind of need the proxying to only go one way. Down the ladder and not upwards.

First of all, your foreign key ids do not need to be virtual. You use virtual for navigation properties (or other entity type properties) only: your foreign key is not a navigation property, it's just an integer.
Now your question is not related to proxies really, it's related to lazy loading (proxies enable lazy loading by default, which is why you are concerned to them, but that's not necessary).
If you only want to load the specified entities, disable lazy loading and do eager loading:
context.Configuration.LazyLoadingEnabled = false;
// Load phonebook entries and users matching your id
var result = content.phonebook
.Include(x => x.Number)
.Include(x => x.User)
.Where(x => x.UserId == 1);
// Load only phonebook entries without users
var result2 = content.phonebook
.Include(x => x.Number)
.Where(x => x.UserId == 1);
That simple
PS: proxies have more advantages to them than lazy loading (like change tracking, etc.). If your problem is lazy loading, turn lazy loading off, not proxies.

In this case it's not proxy creation that's causing misery, it's lazy loading. Disable it by all means, and include the navigation properties you want or project your results specifically:
context.phonebook
.Include(x=> x.Number)
.Where(x=>x.UserId == 1);
// or
context.phonebook
.Where(x=>x.UserId == 1)
.Select(x=> new{
Phonebook = x,
Number = x.Number
});

Related

EF Core NullReferenceException on Related Navigation Property

I have two related models.
public class Offer
{
public long Id { get; set; }
public string OfferCode { get; set; }
public string Description { get; set; }
// more properties
public int ProductId { get; set; }
public virtual Product Product { get; set; }
}
public class Product
{
public long Id { get; set; }
public string Name { get; set; }
// more properties
public virtual ICollection<Offer> Offers { get; set; }
}
I am trying to have an MVC form with a select HTML element where Offers are grouped and products
and have the Product Names serve as optgroups.
To this end, I have a view model that I intend to populate with the grouped Offers and I have a method
to do just that.
private OfferMessageViewModel PrepareViewModel(OfferMessageViewModel viewModel)
{
var offers = _context.Offers.Include(o => o.Product).ToList()
.GroupBy(o => o.Product.Name).ToList();
foreach (var offerGroup in offers)
{
var optionGroup = new SelectListGroup
{
Name = offerGroup.Key
};
foreach (var offer in offerGroup)
{
viewModel.Offers.Add(
new SelectListItem
{
Value = offer.OfferCode,
Text = offer.Description,
Group = optionGroup
}
);
}
}
return viewModel;
}
The code gets tripped up in the GroupBy clause.
o.Product is null even when o.ProductID has a value in it.
The ToList() call right before the GroupBy is not helping.
I have tried removing the virtual modifiers on the related entities
navigation properties but the error persisted.
Installing the NuGet package Microsoft.EntityFrameworkCore.Proxies and
modifying and configuring it as such
services.AddDbContext<ApplicationDbContext>(options =>
options.UseLazyLoadingProxies()
.UseSqlServer(
Configuration.GetConnectionString("DefaultConnection")));
also did not make the error go away.
Is there something else I am missing?
Any help would be greatly appreciated.
EDIT:
It has been suggested that my post might be solved by this SO question. But I get the null reference exception even with lazy loading explicitly turned on.
I have tried the suggested solutions there but still no luck.
I eventually solved it.
Apparently the problem was that the foreign key was an int referencing a primary key of type long.
So I changed
public int ProductId { get; set; }
to
public long ProductId { get; set; }
in the Offer model.
Added the necessary migration, updated the database and now it works.
No more null reference exceptions.
Don't know why I missed that but it's probably a combination of lack of sleep and
a not-so-helpful error message throwing me off in a completely different direction.

GroupBy to identify items with different scores and the number of times they are assigned different scores

I have the following Model classes: Assessment and AssessmentItem. Each assessment is submitted by a unique user for a specific Submission, associated with a Rubric. Each Assessment may have many AssessmentItems, which is composed of a score assigned by the user and the id of the associated rubric item (RubricItemId). Basically, many users can assess a submission using multiple RubricItemId values (as criteria).
public class Assessment
{
[Key]
public int Id { get; set; }
public bool IsCompleted { get; set; }
[ForeignKey("SubmissionId")]
public Submission Submission { get; set; }
public int SubmissionId { get; set; }
[ForeignKey("RubricId")]
public Rubric Rubric { get; set; }
public int RubricId { get; set; }
[ForeignKey("EvaluatorId")]
public ApplicationUser Evaluator { get; set; }
public string EvaluatorId { get; set; }
public IEnumerable<AssessmentItem> AssessmentItems { get; set; }
}
public class AssessmentItem
{
[Key]
public int Id { get; set; }
public int CurrentScore { get; set; }
[ForeignKey("RubricItemId")]
public RubricItem RubricItem { get; set; }
public int RubricItemId { get; set; }
[ForeignKey("AssessmentId")]
public Assessment Assessment { get; set; }
public int AssessmentId { get; set; }
}
I am trying to find the RubricItemId values with different scores per each submission, along with the number of times they were assigned different scores across all submissions. For example, RubricItem #1 was scored differently by users in 10 submissions. However, I do not know where to start. I have the following code to do this without taking into account the submission.
var a = _context.AssessmentItems.GroupBy(ai => ai.AssessmentId)
.Where(da => da.Select(d => d.CurrentScore)
.Distinct()
.Count() == 1
);
This code neither computers the count when RubricItemId is assigned different scores. I wonder how I can move forward from here. Should I use GroupBy. If I want to do this per Submission, I believe there has to be another GroupBy using SubmissionId, right? Any tips and suggestions?
If you could provide some C# format sample data, I could test. This is my attempt - since I am not building against EF you may need an AsEnumerable at some point, which could pull the whole database over.
var ans = _context.Assessments
.GroupBy(a => a.SubmissionId)
.SelectMany(a_sg => a_sg.SelectMany(a => a.AssessmentItems)
.GroupBy(ai => new { ai.RubricItemId, ai.CurrentScore})
.Where(ai_ricsg => ai_ricsg.Count() > 1)
.Select(ai_ricsg => new { ai_ricsg.Key.RubricItemId, DifferentScoreCountPerSubmission = ai_ricsg.Count() })
)
.GroupBy(ric => ric.RubricItemId)
.Select(ric_rig => new { RubricItemId = ric_rig.Key, DifferentScoreCount = ric_rig.Sum(ric => ric.DifferentScoreCountPerSubmission) });
NetMage beat me to it and probably will be closer to what you end up needing. I can confirm his answer should work without needing to materialize the entities:
Since you are wanting details at a submission level, the start of the query should likely be at the Assessment level.
At a high level you will probably be looking to group on the Submission then utilizing SelectMany with a further group-by to drill down to the items you want to count.
var query = _context.Assessments.GroupBy(x => x.SumbissionId)
.SelectMany(x => x.SelectMany(g => g.AssessmentItems
.Select(ai => new { SubmissionId = g.Key, ai.CurrentScore, ai.RubricItemId})
.GroupBy(ai => ai);
This would just be a start, which will get you a structure that can count the distinct combinations of Submission, Score, and RubricItem. I've verified this /w EF6 against a database, so extending out like what NetMage has outlined should be possible without materializing it, or at worst, materializing it as something like the above.
The key thing in this case would be to deal with FKs and particular fields wherever possible rather than pulling back entire entities in the Linq queries as a query like this will probably have a pretty big footprint on DB row touches.

EF one to many on part of composite primary key

I have three layers of tables in an existing database and I'm trying to include the bottom level records when I get the middle level data... This should be a one to many relationship - for shipment x with product y there are z analysis results.
public class Shipment
{
[Key]
public int Id { get; set; }
public string ShipName { get; set; }
public DateTime ShipmentDate { get; set; }
}
public class ShipmentDetails
{
[ForeignKey ("ShipmentId")]
public int Id { get; set; }
[ForeignKey ("ProductId")]
public int ProductId { get; set; }
Public double Weight { get; set; }
public virtual ShippingAnalysis Analysis { get; set; }
}
public class ShipmentAnalysis
{
[ForeignKey ("ShipmentId")]
public int Id { get; set; }
[ForeignKey ("ProductId")]
public int TenantId { get; set; }
[ForeignKey ("MetricId")]
public int MetricId { get; set; }
Public double Result { get; set; }
}
I'm using the fluent api way of defining the composite primary keys.
modelBuilder.Entity<ShippingDetail>()
.HasKey(c => new { c.ShipmentId, c.ProductlId });
modelBuilder.Entity<ShippingAnalysis>()
.HasKey(c => new { c.ShipmentId, c.ProductId, c.MetricId });
I get the Shipping detail with the (one to many) analysis records.
var results = _context.ShippingDetail.Include(sd => sd.Analysis)
.Where(sd => sd.ShipmentId == id);
This does not return a result in postman, but through the browser returns malformed JSON. If I drop the include, it works fine.
The problem is not composite key, but navigation property (hence relationship definition). The navigation property at (one) side (when present) must be a collection and navigation property at (many) side should be reference - see Relationships - Definition of Terms.
According to
modelBuilder.Entity<ShippingDetail>()
.HasKey(c => new { c.ShipmentId, c.ProductlId });
modelBuilder.Entity<ShippingAnalysis>()
.HasKey(c => new { c.ShipmentId, c.ProductId, c.MetricId });
the relationship should be ShippingDetail (one) -> (many) ShippingAnalysis, hence
public virtual ShippingAnalysis Analysis { get; set; }
property of ShippingDetail must be
public virtual ICollection<ShippingAnalysis> Analysis { get; set; }
This should be enough for EF Core to determine the correct composite FK columns. But if you want to be hundred percent sure (being explicit never hurts), add the following fluent configuration:
modelBuilder.Entity<ShippingDetail>()
.HasMany(e => e.Analysis)
.WithOne() // make sure to specify navigation property if exists, e.g. e => e.NavProp
.HasForeignKey(e => new { e.ShipmentId, e.ProductId });
P.S. Remove all these [ForeignKey] data annotations. They do different things depending on whether they are applied on FK property or navigation property, and for sure don't do what you think, and sometimes may actually lead to unexpected behaviors. Based on my experience with EF Core relationships, either let EF Core conventions do their job, or use fluent API.

Entity Framework - Stop Lazy Loading Related Entities On Demand?

I have Entity Framework set up and it works fine most of the time I need it. I have a structure like so
public partial class Topic : Entity
{
public Guid Id { get; set; }
public string Name { get; set; }
public DateTime CreateDate { get; set; }
public virtual Post LastPost { get; set; }
public virtual Category Category { get; set; }
public virtual IList<Post> Posts { get; set; }
public virtual IList<TopicTag> Tags { get; set; }
public virtual MembershipUser User { get; set; }
public virtual IList<TopicNotification> TopicNotifications { get; set; }
public virtual IList<Favourite> Favourites { get; set; }
public virtual Poll Poll { get; set; }
}
As you can see I have a number of related entities which are lists. These are mapped as standard and are lazy loaded so I can call Topic.Posts or Topic.TopicNotifications etc... (Mappings below)
HasOptional(t => t.LastPost).WithOptionalDependent().Map(m => m.MapKey("Post_Id"));
HasOptional(t => t.Poll).WithOptionalDependent().Map(m => m.MapKey("Poll_Id"));
HasRequired(t => t.Category).WithMany(t => t.Topics).Map(m => m.MapKey("Category_Id"));
HasRequired(t => t.User).WithMany(t => t.Topics).Map(m => m.MapKey("MembershipUser_Id"));
HasMany(x => x.Posts).WithRequired(x => x.Topic).Map(x => x.MapKey("Topic_Id")).WillCascadeOnDelete();
HasMany(x => x.TopicNotifications).WithRequired(x => x.Topic).Map(x => x.MapKey("Topic_Id")).WillCascadeOnDelete();
HasMany(t => t.Tags)
.WithMany(t => t.Topics)
.Map(m =>
{
m.ToTable("Topic_Tag");
m.MapLeftKey("TopicTag_Id");
m.MapRightKey("Topic_Id");
});
This is all an well. But on a couple of occasions I have the need to manually populate Topic.Posts and Topic.Favorites.
But if I try and set Topic.Posts = SomeCollection it triggers the lazy load and loads all the posts first, and then lets me set my collection so I get two sets of sql executed (The first I don't want)
Is there anyway, to switch off the lazy loading manually on demand for just when I want to set the collection manually?
Hope that makes sense... :/
You would be better to turn off lazy loading by default and instead specify when you want to load the extra data in the first place. EF is set up to allow Eager loading by using the .Include() function on your query, with lazy loading it can get messy if you start turning it on/off for various functions, you're better to just turn it off and manage what/when you want data loaded if you're feeling a need to turn it off.
See https://msdn.microsoft.com/en-nz/data/jj574232.aspx for specific examples and a breakdown of the different ways you can eager/lazy load data. The first example shows how you could pull through posts off a blog, which is similar to what you are wanting to acheive.
var topics = context.Topics
.Include(t => t.Posts)
.ToList();
I am not aware of an approach targeted to this exact scenario so I'd have to go with a temporary disable/enable of lazy loading.
using(var context = new MyContext())
{
context.Configuration.LazyLoadingEnabled = false;
// do your thing using .Include() or .Load()
context.Configuration.LazyLoadingEnabled = true;
}
Note however that this is a global configuration so there might be a concurrency problem if that happens in your scenario.
I would not recommend turning off lazing loading on a per request basis. As AllMadHare suggests, you could turn off lazy loading entirely, but that could force changes to how you load all data. I would recommend removing the virtual keyword from Posts so that your class looks like this:
public partial class Topic : Entity
{
public Guid Id { get; set; }
public string Name { get; set; }
public DateTime CreateDate { get; set; }
public virtual Post LastPost { get; set; }
public virtual Category Category { get; set; }
public IList<Post> Posts { get; set; }
public virtual IList<TopicTag> Tags { get; set; }
public virtual MembershipUser User { get; set; }
public virtual IList<TopicNotification> TopicNotifications { get; set; }
public virtual IList<Favourite> Favourites { get; set; }
public virtual Poll Poll { get; set; }
}
Per the documentation found here: https://msdn.microsoft.com/en-us/data/jj574232.aspx#lazyOffProperty this will allow you to lazy load all other navigation properties and eager load posts if you need to.
Since you are using lazy loading, you must have proxies generated for your classes and collection properties.
Replacing these proxy collection properties with your own collections seems pretty weird design to me. You loose change tracking and most probably gain a couple of other strange side effects.
I would recommend to either use proxies/lazy loading and give up the idea of replacing the collections, or stand back from using proxies and gain full control on the generated POCO classes.
Which of both approaches suits best for your needs depends on your overall usage of the entity framework.

LINQ to Entities query error

I am encountered an error that I am not familier with. I tried to google with no success.
I wrote the following query where I am having this error.
The entity or complex type 'MyWebProject.Models.UserDetail' cannot be constructed in a LINQ to Entities query.
The query:
UsersContext db = new UsersContext();
var userdata = (from k in db.UserDetails
where k.UserId == WebSecurity.CurrentUserId
select new UserDetail()
{
FullName = k.FullName,
Email = k.Email,
About = k.About,
Link = k.Link,
UserSchool = new School()
{
SchoolId = k.UserSchool.SchoolId,
SchoolName = k.UserSchool.SchoolName
},
UserCourse = new Course()
{
CourseId=k.UserCourse.CourseId,
CourseName=k.UserCourse.CourseName
},
Country=k.Country
}).FirstOrDefault();
Class:
public class UserDetail
{
public int Id { get; set; }
public int UserId { get; set; }
public string FullName { get; set; }
public string Link { get; set; }
public bool? Verified { get; set; }
public string Email { get; set; }
public string About { get; set; }
public School UserSchool { get; set; }
public Course UserCourse { get; set; }
public string Country { get; set; }
}
public class School
{
public int SchoolId { get; set; }
public string SchoolName { get; set; }
public string Country { get; set; }
}
public class Course
{
public int CourseId { get; set; }
public string CourseName { get; set; }
public School School { get; set; }
}
Any idea what went wrong??
It looks like it is due to how you are creating the complex properties School and Course in the middle of the query. It would be better to select the User (remove the select transformation), then use navigation properties to access those objects instead of building them manually. The navigation are meant for this as long as you have the proper relations built with foreign keys.
UsersContext db = new UsersContext();
var userdata = (from k in db.UserDetails
where k.UserId == WebSecurity.CurrentUserId})
.FirstOrDefault();
// access navigation properties which will perform the joins on your behalf
// this also provides for lazy loading which would make it more effecient. (it wont load the school object until you need to access it)
userdata.School
userdata.Course
MSDN article about navigation properties: http://msdn.microsoft.com/en-us/library/vstudio/bb738520(v=vs.100).aspx
This should give you what you want. It will load your objects as part of the query (and not rely on lazy loading).
UsersContext db = new UsersContext();
var userdata = db.UserDetails.Include(x => x.UserSchool)
.Include(x => x.UserCourse)
.Include(x => x.Country)
.Where(x => x.UserId == WebSecurity.CurrentUserId)
.FirstOrDefault();
I think it's because your entity has the same name of the object you're trying to create. Try renaming the object you want to return back. If you want to return the same type as your entity try the eager loading with .Include("relationshipname") feature.
A great answer from #Yakimych is given below.
You cannot (and should not be able to) project onto a mapped entity. You can, however, project onto an annonymous type or onto a DTO:
public class ProductDTO
{
public string Name { get; set; }
// Other field you may need from the Product entity
}
And your method will return a List of DTO's.
public List<ProductDTO> GetProducts(int categoryID)
{
return (from p in db.Products
where p.CategoryID == categoryID
select new ProductDTO { Name = p.Name }).ToList();
}
Mapped entities in EF basically represent database tables. If you project onto a mapped entity, what you basically do is partially load an entity, which is not a valid state. EF won't have any clue how to e.g. handle an update of such an entity in the future (the default behaviour would be probably overwriting the non-loaded fields with nulls or whatever you'll have in your object). This would be a dangerous operation, since you would risk losing some of your data in the DB, therefore it is not allowed to partially load entities (or project onto mapped entities) in EF.
For more details please go to the following link:
The entity cannot be constructed in a LINQ to Entities query

Categories

Resources