Linking a third model in EF CORE - entity-framework

This is the EF CORE setup I have at the moment.
Telephone model
[ForeignKey("TelNoTypeID")]
public int? TelNoTypeID { get; set; }
public DropDown TelNoType { get; set; }
Person model
public ICollection<Telephone> TelephoneIDs { get; set; }
I've also tried using
public virtual ICollection<Telephone> TelephoneIDs { get; set; }
DropDown model
public int DropDownID { get; set; }
public string DisplayText { get; set; }
Person controller
public async Task<IActionResult> Details(int? id)
{
var person = await _context.Persons
.Include(p => p.Gender)
.Include(p => p.Title)
.Include(p => p.AddressIDs)
.Include(p => p.TelephoneIDs)
.SingleOrDefaultAsync(m => m.PersonID == id);
So a collection of Telephone numbers are stored against the Person model.
This brings back from the database and displays on the screen the numeric value from the field
Telephones.TelNoTypeID
The equivalent SQL to what I have is
select Telephones.TelNoTypeID from Person
inner join Telephones on Telephones.PersonID = Person.PersonID
I need to display the linked text value instead which is stored in
DropDown.displaytext
The equivalent SQL to what I need is
select DropDown.displaytext from Person
inner join Telephones on Telephones.PersonID = Person.PersonID
inner join DropDown on DropDown.DropDownID = Telephones.TelNoTypeID
I've tried using
.ThenInclude(p => p.<something>)
but the Dot notation just refers to the generic collection properties/methods, not back to my person, dropdown or telephone models
I've linked the Person model directly to the Dropdown model using and displaying the dropdown.displaytext with
Person model
[ForeignKey("GenderID")]
public int? GenderID { get; set; }
public virtual DropDown Gender { get; set; }
but despite putting the same code into telephone, it's not working
How can I link a third model (DropDown) to a second model (Telephone)?
Thanks

I was trying to link to the DropDown model, turns out I needed to link to the property, so
var person = await _context.Persons
.Include(p => p.Gender)
.Include(p => p.Title)
.Include(p => p.AddressIDs)
.Include(p => p.TelephoneIDs)
.ThenInclude(p => p.TelNoType)
//.Include("TelephoneIDs.TelNoType.DisplayText")
.SingleOrDefaultAsync(m => m.PersonID == id);
I don't understand why, I'm presuming it's because the links already exist between the 3 models and the ThenInclude points to the exact property I'm trying to link with/to.

Related

Entity Framework Core navigations not updating

I'm using Entity Framework Core v5.0.3 for this.
I'm making a simple information app for users where each user can have favourite colours.
Person model
public class Person
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int PersonId { get; set; }
[MaxLength(50)]
public string FirstName { get; set; }
[MaxLength(50)]
public string LastName { get; set; }
public bool IsAuthorised { get; set; }
public bool IsValid { get; set; }
public bool IsEnabled { get; set; }
public virtual ICollection<FavouriteColour> FavouriteColours { get; set; }
}
Favourite colour model
public class FavouriteColour
{
[Key]
public int PersonId { get; set; }
public virtual Person Person { get; set; }
[Key]
public int ColourId { get; set; }
public virtual Colour Colour { get; set; }
}
Colour model
public class Colour
{
[Key]
public int ColourId { get; set; }
public string Name { get; set; }
public bool IsEnabled { get; set; }
public virtual IList<FavouriteColour> FavouriteColours { get; set; }
}
In my database context I've defined them as so
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<FavouriteColour>()
.HasKey(c => new { c.PersonId, c.ColourId });
modelBuilder.Entity<FavouriteColour>()
.HasOne(fc => fc.Person)
.WithMany(p => p.FavouriteColours)
.HasForeignKey(p => p.PersonId);
modelBuilder.Entity<FavouriteColour>()
.HasOne(fc => fc.Colour)
.WithMany(c => c.FavouriteColours)
.HasForeignKey(c => c.ColourId);
}
Now in the app a user can add or remove favourite colours so a new user object is received in the controller which calls the repository update
public async Task<IActionResult> PutPerson(int id, Person person)
{
peopleRepository.Update(person);
}
The repository then does the update
public void Update(Person person)
{
_context.Update(person);
}
After doing await _context.SaveChangesAsync(); none of the colours change. I thought the purpose of all these models and such was so the colours would change automatically?
I setup a FavouriteColourRepository instead to do the updates like this
public async Task<bool> Update(int personId, ICollection<FavouriteColour> favouriteColours)
{
// empty their favourite colours
_context.FavouriteColours.RemoveRange(_context.FavouriteColours.Where(fc => fc.PersonId == personId);
// add new favourite colours
_context.FavouriteColours.AddRange(favouriteColours);
return true;
}
And I changed my controller to this
public async Task<IActionResult> PutPerson(int id, Person person)
{
peopleRepository.Update(person);
bool valid = await favouriteColourRepository.Update(id, person.FavouriteColours);
}
But for some reason I can't figure out
_context.FavouriteColours.RemoveRange(_context.FavouriteColours.Where(fc => fc.PersonId == personId);
actually alters the favouriteColours parameter and forces the last state into it which creates duplicate primary keys and the inserting fails.
So why do the new favourite colours never get inserted and why is my favouriteColours parameter being edited when I'm trying to clear all the colours a user already has?
why do the new favourite colours never get inserted
The call _context.Update(person); puts the Person entity and all is related FavouriteColour entity in Modified state. Hence, on the next SaveChanges call EF is supposed to submit update commands to the database.
If you modify any property of the Person entity, you'll find that the person has been updated correctly. The issue with the FavouriteColour entity is that it contains nothing but the primary key properties, and EF does not modify the key property (or any property that is part of a composite primary key). You can test this with the following code -
var fc = dbCtx.FavouriteColours
.FirstOrDefault(p => p.PersonId == 1 && p.ColourId == 4);
fc.ColourId = 6; // CoulourId is part of primary key
dbCtx.SaveChanges();
and you will be met with an exception -
The property 'FavouriteColour.ColourId' is part of a key and so cannot
be modified or marked as modified. ...
Therefore, EF will not even generate any update command for the FavouriteColours, even though all of them are marked as Modified.
To update the Person and all its FavouriteColour you need to do something like -
// fetch the exising person with all its FavouriteColours
var existingPerson = dbCtx.Persons
.Include(p => p.FavouriteColours)
.FirstOrDefault(p => p.Id == id);
// modify any properties of Person if you need
// replace the existing FavouriteColours list
existingPerson.FavouriteColours = person.FavouriteColours;
// save the changes
dbCtx.SaveChanges();
This will -
delete any FavouriteColour that are in the existing list but not in the new list
insert any FavouriteColour that are in the new list but not in the existing list.
With your repositories in place, its up to you how you implement this.
why is my favouriteColours parameter being edited when I'm trying to
clear all the colours a user already has
Before this operation you called _context.Update(person);. Therefore, Person is now being tracked in Modified state as an existing entity. Then when you called -
_context.FavouriteColours.RemoveRange(_context.FavouriteColours.Where(fc => fc.PersonId == personId));
the _context.FavouriteColours.Where(fc => fc.PersonId == personId) part, fetched the existing FavouriteColours of that Person from the database. So these fetched FavouriteColours are added to the person's FavouriteColours list (because it is being tracked).
For example, lets say in your controller you received a Person entity with 4 FavouriteColour, and the database has 3 FavouriteColour for this person. After calling _context.Update(person);, the person is being tracked with a list of 4 FavouriteColour. Then when you fetched this person's existing FavouriteColours from the database, the person is being tracked with a list of total 7 FavouriteColour.
I hope that shades any light.

Entity Framework Core 3.1 is throwing "Include has been used on non entity queryable" exception

I have an ASP.NET Core 3.1 based project where I am using Entity Framework Core 3.1 as an ORM.
I have the following two entity models
public class PropertyToList
{
public int Id { get; set; }
public int ListId { get; set; }
public int UserId { get; set; }
public int PropertyId { get; set; }
// ... Other properties removed for the sake of simplicity
public virtual Property Property { get; set; }
}
public class Property
{
public int Id { get; set; }
// ... Other properties removed for the sake of simplicity
public int TypeId { get; set; }
public int StatusId { get; set; }
public int CityId { get; set; }
public virtual Type Type { get; set; }
public virtual Status Status { get; set; }
public virtual City City { get; set; }
}
I am trying to query all properties where a user has relation to. The PropertyToList object tells me if a user is related to a property. Here is what I have done
// I start the query at a relation object
IQueryable<Property> query = DataContext.PropertyToLists.Where(x => x.Selected == true)
.Where(x => x.UserId == userId && x.ListId == listId)
// After identifying the relations that I need,
// I only need to property object "which is a virtual property in" the relation object
.Select(x => x.Property)
// Here I am including relations from the Property virtual property which are virtual properties
// on the Property
.Include(x => x.City)
.Include(x => x.Type)
.Include(x => x.Status);
List<Property> properties = await query.ToListAsync();
But that code is throwing this error
Include has been used on non entity queryable
What could be causing this problem? How can I fix it?
Put your includes right after referencing the parent entity.
You can do ThenInclude to bring the child entities of the included entities also. You'll need to do one Include for each ThenInclude.
Then you can do your selecting after including / filtering. Something like:
var query = DataContext.PropertyToLists
.Include(p => p.Property).ThenInclude(p => p.City)
.Include(p => p.Property).ThenInclude(p => p.Type)
.Include(p => p.Property).ThenInclude(p => p.Status)
.Where(p => p.Selected == true && p.UserId == userId && p.ListId == listId)
.Select(p => p.Property);
An observation, your domain models PropertyToList and Property both have virtual properties. On top of that you are using Include operator to select these properties.
This is not necessary, when property is defined with virtual, then it will be lazy loaded. So an Include is not needed. Lazy loading is not the recommended way, using include is better so you select only graph properties that are required.

Compare property of model in the list with the constant value

Its a simple question but I am starting with EF and dont know:
I have two classes (db objects):
Company (simplified)
public class Company
{
public int ID { get; set; }
public string Name { get; set; }
public virtual ICollection<UserGroup> UserGroups { get; set; }
}
and userGroup (there are many users in the group):
public class UserGroup
{
public int ID { get; set; }
public string Name { get; set; }
public virtual ICollection<ApplicationUser> Users { get; set; }
public virtual ICollection<Company> Companies { get; set; }
}
In controller I need to select the companies which have a specific UserGroupID. I dont know how to write the select condition. I mean something like:
var currentUser = db.Users.Find(User.Identity.GetUserId());
var companies = db.Companies
.Include(c => c.Address)
.Include(c => c.User)
.Where(c => c.UserGroups == currentUser.UserGroup)
;
It would be helpful to see your ApplicationUser class, but I suppose it has a navigation property to UserGroup? If it does, you could do it like this:
db.Users.Where(u => u.Id == User.Identity.GetUserId())
.SelectMany(u => u.UserGroups)
.SelectMany(g => g.Companies)
.Distinct();
I like to write the where clause over entity I want to return. In your case:
db.Companies.Where(c => c.UserGroups.Any(g => g.ID == currentUser.UserGroup.ID));
This way you don't have to add the Distinct().

EF5 List of one to one dont materialized every entities

I am using EF5.0 CF let's consider theses entities (simplified here):
public class Catalog
{
public int Id { get; set; }
public bool IsActive { get; set; }
public string Name { get; set; }
public ICollection<PricedProduct> Products { get; set; }
}
public class PricedProduct
{
public int Id { get; set; }
public bool IsActive { get; set; }
public Product Product { get; set; }
public Price Price { get; set; }
}
public class Price
{
public int Id { get; set; }
}
public class Product
{
public int Id { get; set; }
public string Name {get; set;}
}
They are configured with the fluent API :
//For the Catalog entity
ToTable("Catalog", "Catalog");
this.Property(t => t.Name).HasColumnType("nvarchar").HasMaxLength(100).IsRequired();
this.HasMany<PricedProduct>(t => t.Products).WithMany().
Map(mc =>
{
mc.ToTable("CatalogPricedProduct", "Catalog");
mc.MapLeftKey("PricedProductID");
mc.MapRightKey("CatalogID");
});
//For the PricedProduct entity
ToTable("PricedProducts", "Catalog");
HasRequired(t => t.Product).WithOptional().Map(m=>m.MapKey());
HasRequired(t => t.Price).WithOptional().Map(m => m.MapKey());
//For the Product entity
ToTable("Products", "Catalog");
this.Property(t => t.Name).HasColumnType("nvarchar").HasMaxLength(100).IsRequired();
//For the Price entity
ToTable("Prices", "Catalog");
So basically I have a catalog which have n:n relationship with PricedProduct that have two 1:1 relationship with Product and Price
I get those entities with this linq query :
var qy = from cata in this.Set<Catalog>().Include("Products")
.Include("Products.Product")
.Include("Products.Price")
where cata.Name == "name"
select cata;
return qy.FirstOrDefault();
Everything works well as long as two PricedProduct does not share the same product or the same price.
Meaning that, in the PricedProducts table the PriceProduct are retrieved and materialized correctly as long as the Product or the Price FK are "unique", if another PricedProduct have the same FK value on price for instance, price wont be loaded in the concerned PricedProduct.
I have quickly check the SQL query generated and it looks fine, it feels like EF fail to materialize two instances of the same object in a same graph ??
Anyone knows what to do or what is wrong with my code ?
thank a lot
That is because your understanding of your model is not correct. If multiple PricedProduct can have same Price or same Product you cannot map it as one-to-one relationship but as one-to-many (one price can be assigned to many priced products - same for product). You need:
ToTable("PricedProducts", "Catalog");
HasRequired(t => t.Product).WithMany();
HasRequired(t => t.Price).WithMany();

How to modify this Entity framework code first code?

I am brand new to Entity Framework code first, so any help or direction would be much appreciated.
I currently have the following classes:
public partial class Customer
{
public int Id { get; set; }
private ICollection<Address> _addresses;
}
public partial class Address
{
public int Id { get; set; }
public string Street { get; set; };
public string City { get; set; };
public string Zip { get; set; };
}
and the following
public partial class CustomerMap : EntityTypeConfiguration<Customer>
{
public CustomerMap()
{
this.ToTable("Customer");
this.HasKey(c => c.Id);
this.HasMany<Address>(c => c.Addresses)
.WithMany()
.Map(m => m.ToTable("CustomerAddresses"));
}
}
This works as I would expect, and creates a Customer, Address and CustomerAddresses table for the mapping. Now for my question.. what would I do if I needed to modify the code to produce the following...
I want to add a CompanyCode attribute to the "CustomerAddresses" table... and then instead of constructing a collection of addresses.. i want to be able to construct a hashtable, where the key is the CompanyCode, and the value is the collection of addresses.
So if I had the following:
Customer
ID C1
Address
ID A1
ID A2
CustomerAddresses
CustomerID C1
AddressID A1
CompanyCode ABC
CustomerID C1
AddressID A2
CompanyCode ABC
CustomerID C1
AddressID A2
CompanyCode XYZ
so then, Customer.Addresses["ABC"] would return a collection of addresses with ID, A1 and A2. Whereas Customer.Addresses["XYZ"] would return a collection of addresses with ID A2.
Any direction/help would be much appreciated... thanks.
As far as I can tell it isn't possible to introduce such a navigation property with an indexer. Your indexer is actually a query and you must express this as a query. The only way I see is that you leave the navigation collection as is and introduce a second (not mapped) property that uses the navigation collection for the filter. The big drawback is that such a filter would happen in memory with LINQ-to-Objects and requires that you always load the full collection first from the database (by eager or lazy loading for example) before you filter the collection.
I would probably leave such a filter out of the entity itself and implement it in a repository or service class or generally the place/module where you load the entities from the database.
The first thing you need to do is exposing the CustomerAddresses table as an entity in your model because with your additional custom property CompanyCode you can't use a many-to-many relationship anymore, instead you need two one-to-many relationships. The new entity would look like this:
public partial class CustomerAddress
{
public int CustomerId { get; set; }
// public Customer Customer { get; set; } // optional
public int AddressId { get; set; }
public Address Address { get; set; }
public string CompanyCode { get; set; }
}
And the Customer needs to be changed to:
public partial class Customer
{
public int Id { get; set; }
public ICollection<CustomerAddress> CustomerAddresses { get; set; }
}
You need to change the mapping to:
public CustomerMap()
{
this.ToTable("Customer");
this.HasKey(c => c.Id);
this.HasMany(c => c.CustomerAddresses)
.WithRequired() // or .WithRequired(ca => ca.Customer)
.HasForeignKey(ca => ca.CustomerId);
}
And create a new mapping for the new entity:
public CustomerAddressMap()
{
this.ToTable("CustomerAddresses");
this.HasKey(ca => new { ca.CustomerId, ca.AddressId, ca.CompanyCode });
// or what is the PK on that table?
// Maybe you need an Id property if this key isn't unique
this.HasRequired(ca => ca.Address)
.WithMany()
.HasForeignKey(ca => ca.AddressId);
}
Now, in some service class you could load the filtered addresses:
public List<Address> GetAddresses(int customerId, string companyCode)
{
return context.CustomerAddresses.Where(ca =>
ca.CustomerId == customerId && ca.CompanyCode == companyCode)
.ToList();
}
Or, if you want to load the customer together with the filtered addresses:
public Customer GetCustomer(int customerId, string companyCode)
{
var customer = context.Customer.SingleOrDefault(c => c.Id == customerId);
if (customer != null)
context.Entry(customer).Collection(c => c.CustomerAddresses).Query()
.Where(ca => ca.CompanyCode == companyCode)
.Load();
return customer;
}
The last example are two database queries.
In the Customer entity you could use a helper property that projects the addresses out of the CustomerAddresses collection:
public partial class Customer
{
public int Id { get; set; }
public ICollection<CustomerAddress> CustomerAddresses { get; set; }
public IEnumerable<Address> Addresses
{
get
{
if (CustomerAddresses != null)
return CustomerAddresses.Select(ca => ca.Address);
return null;
}
}
}
Keep in mind that this property does not query the database and the result relies on what is already loaded into CustomerAddresses.