One-to-many with Join Table in Entity Framework 6 - c#

I realize this has been answered more than once, here for example, but nothing seems to be working for me and I'm not sure why.
My database has Sites and Users. A User might own a Site, or he might work at a Site. He might own Sites A and C, but work on Site B. However, each site has only one worker and one owner. Therefore, I have created a join table structure with the following three tables: User, Site, and User_Site, and User_Site contains a column called role that can be either "worker" or "owner".
To simplify things, I have created two views, view_Worker and view_Owner. view_Owner, for example, is
SELECT User_Site.site_id, User.*
FROM User_Site
JOIN User ON User_Site.user_id = User.user_id
WHERE User_Site.role = "owner"
Therefore, each row in view_Owner contains all of the information on User and the site_id for which the User is joined as an "owner".
Now, I'm trying to build an API around this database using Entity Framework 6. I've gotten a number of errors trying a number of different things, so I'm not sure which errors to post here. I'll post my most recent error:
dbContext.cs
public DbSet<User> Users { get; set; }
public DbSet<Site> Sites { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<Site>()
.HasOptional<User>(s => s.Owner)
.WithMany()
.Map(m =>
{
m.MapKey("site_id").ToTable("view_Owner");
});
}
user.cs
[Table("User")]
public class User
{
[Key, Column("user_id")]
public int ID { get; set; }
}
site.cs
[Table("Site")]
public class Site
{
[Key, Column("site_id")]
public int ID { get; set; }
public virtual User Owner { get; set; }
}
The error message I get with this configuration is
The specified table 'view_Owner' was not found in the model. Ensure
that the table name has been correctly specified.
So, the next step is to try and add this table to the model. I change [Table("User")] to [Table("view_Owner")] and I get the following error message:
(66,6) : error 3021: Problem in mapping fragments starting at line
66:Each of the following columns in table User is mapped to multiple
conceptual side properties: User.site_id is mapped to
(66,6) : error 3025: Problem in mapping fragments starting at line
66:Must specify mapping for all key properties (User.user_id) of table
User.
Clarification
Ideally, I'd like a way to simply tell EntityFramework to load the Owner property on Site from the view_Owner table by joining view_Owner.site_id to the Site.site_id primary key. But, since the Owner property is still of type User, it doesn't need a new definition.

Related

Entity Framework Core 6 adding (or referencing when already exists) data in a Many-to-Many relationship

I have been struggling to get a many-to-many relationship up and running. I have followed the Microsoft's example for EF Core 5+. My Join table is getting created and I can add entries to all three tables named: SpecificationTest, SpecificationDeviceSpecificationTest and SpecificationDevice.
I am however having some difficulties populating the join table, when the 'SpecificationDevice' already exists.
The simplified entities look as follows:
public class SpecificationTest
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
public ICollection<SpecificationDevice> SpecificationDevices { get; set; }
}
public class SpecificationDevice
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
[Required]
public string Name { get; set; }
[Required]
public string SerialNumber { get; set; }
public ICollection<SpecificationTest> SpecificationTests { get; set; }
}
The join table is automatically created, but I have added the following in the OnModelCreating method as well, even though it is possibly redundant:
modelBuilder.Entity<SpecificationTest>()
.HasMany(a => a.SpecificationDevices)
.WithMany(a => a.SpecificationTests);
modelBuilder.Entity<SpecificationDevice>()
.HasMany(a => a.SpecificationTests)
.WithMany(a => a.SpecificationDevices);
The DbContext also contains the following DbSets:
public DbSet<SpecificationTest> SpecificationTests { get; set; }
public DbSet<SpecificationDevice> SpecificationDevices { get; set; }
By design, every SpecificationTest can have multiple SpecificationDevices. However, the SpecificationDevice table itself can only contain a single entry per unique device (Unique by combination of Serial and Name).
Adding a new SpecificationDevice works as expected. EF creates the entry in the SpecificatonDevice table, and correctly populates the SpecificationDeviceSpecificationTest join table as well.
The problem I am having is that if the SpecificationDevice being added already exists, I merely want to select the ID of that device and add it to the Join table. Currently it tries to add another entry with the ID I have selected, and then throws the following exception:
Error: System.InvalidOperationException: The instance of entity type 'SpecificationDevice' cannot be tracked because another instance with the key value '{Id: 1}' is already being tracked.
Here is the code I use to try and add the entities. Disclaimer, I use the Repository pattern with a UnitOfWork to do the work, but the code comes down to the same.
Note: The specificationTest.SpecificationDevices collection already contains data at this stage, where the SpecificationDevice can be a new device or an existing device.
var existingDevices = await dbContext.SpecificationDevices.AsNoTracking().ToArrayAsync();
foreach (var specDevice in specificationTest.SpecificationDevices)
{
var existingSpecDevice = existingDevices.FirstOrDefault(a => a.Name.Equals(specDevice.Name) && a.SerialNumber.Equals(specDevice.SerialNumber));
if (existingSpecDevice is not null)
{
specDevice.Id = existingSpecDevice.Id;
}
}
await dbContext.SpecificationTests.AddAsync(specificationTest);
await dbContext.SaveChangesAsync();
I would expect that because I selected the ID of the existing SpecificationDevice, that EF will automatically populate the Join table with the correct values (SpecificationTestId just created and the already existing SpecificationDeviceId) This is not the case and I get the exception in the block quote above.
The approach of selecting the ID above works perfectly when trying to add a one-to-one entry when the unique entry already exists, but not with Many-to-many relationship.
Am I doing something wrong? Am I missing something?

How to migrate an already set up one-to-many relationship in Entity Framework?

I have a C# WPF application using an SQLite database with Entity Framework. I have a Contact class which can have multiple messages, so there is a one-to-many relationship, set up the following way (simplified version):
public class Message {
public int PK { get; set; }
public int SenderKey { get; set; }
public Contact Sender { get; set; }
}
public class Contact {
public int PK { get; set; }
public ICollection<Message> Messages { get; set; }
}
Then I set the relationship using the Fluent API, the following way:
protected override void OnModelCreating(DbModelBuilder modelBuilder) {
...
modelBuilder.Entity<Message>()
.HasOptional(e => e.Sender)
.WithMany(e => e.Messages)
.HasForeignKey(e => e.SenderKey);
}
I'm going to have lots of data (and also have lots of troubles with this circular dependency, especially on editing the objects in detached mode) so it's not a good idea to have that Messages collection in the memory for every contact. To avoid this, I'd like to get rid of that list, so it would be great to implement the 'Convention 1' from the docs (to just have the Contact object in the Message class and that's all).
The problem with this solution is that my app is already published, so I can't just simply change the structure, I need a migration. My question is that how can I migrate this kind of relation set up by the Fluent API?
I tried to remove the relationship from the OnModelCreating, but I got the following exception when I started the app:
System.Data.SQLite.SQLiteException: SQL logic error no such column: Extent1.Sender_PK (what is that Extent1 table?)
Finally I found the solution. I didn't have to implement a specific migration, just modify the following things:
Remove the relationship setup from the OnModelCreating
The previous step caused that "sql logic error" posted in the question. This was because the foreign key column name wasn't specified and the Entity Framework searched for a default column, which is in fact Sender_PK. So, in order to solve this, I added an annotation in the Message class, which tells the Entity Framework what is the foreign key column name for that Contact object:
public class Message {
public int PK { get; set; }
public int SenderKey { get; set; }
[ForeignKey("SenderKey")]
public Contact Sender { get; set; }
}
Remove the message list reference (public ICollection<Message> Messages { get; set; }) from the Contact class.
So, after this three step modification I had the one-to-many relationship between the two tables and I could get rid of that list. Nothing else needed, it works perfectly with the old databases.

Mapping One-to-Zero-or-One with EF7

I am currently in the process of cleaning up a fairly large database. Part of the database has a relationship which is a one-to-zero-or-one mapping. Specifically:
User -> UserSettings
Not all users will have user settings, but a user setting cannot exist without the user. Unfortunately, the tables already exist. User has an PK ID. UserSettings has a PK ID and a column, User_Id_Fk which, at this point in time, is not a true FK (there is no relationship defined).
I'm in the process of fixing that and have done so from the DB perspective through SQL and have confirmed with tests. (Added the FK constraint. Added a unique constraint on User_Id_Fk.) This was all done on the UserSettings table. (Note: I am not using EF Migrations here. I have to manually write the SQL at this point in time.)
However, I now need to wire up an existing application to properly handle this new mapping. The application is using ASP.NET Core 1.0 and EF7. Here are (shortened) versions of the existing data models.
public class User
{
public int Id { get; set; }
public virtual UserSettings UserSettings { get; set; }
}
public class UserSettings
{
public int Id { get; set; }
[Column("User_Id_Fk")]
public int UserId { get; set; }
[ForeignKey("UserId")]
public virtual User User { get; set; }
}
I have this Fluent Mapping as well:
builder.Entity<UserSettings>()
.HasOne(us => us.User)
.WithOne(u => u.User)
.IsRequired(false);
When I go to run the application and access these items in the database, I get this error followed with a cryptic set of messages that has no information relating directly back to my application.:
ArgumentNullException: Value cannot be null.
Parameter name: navigation
Microsoft.Data.Entity.Utilities.Check.NotNull[T] (Microsoft.Data.Entity.Utilities.T value, System.String parameterName) <0x10d28a650 + 0x00081> in <filename unknown>, line 0
After doing research, someone had mentioned that the ID of the UserSettings class must be the same as the foreign key, like so:
public class UserSettings
{
[Key, ForeignKey("User")]
public int Id { get; set; }
public virtual User User { get; set; }
}
I don't really have this as an option as the DB is being used for other applications I have no control over at this point. So, am I stuck here? Will I just have to maintain a 1:many mapping (which could happen now, though it hasn't) and not have proper constraints for a 1:0..1 mapping?
Update
Looking at octavioccl's answer below, I tried it out without any success. However, I then removed User from the mapping in UserSettings (but I left UserId). Everything appeared to work as far as I can tell. I'm really confused what is going on here, however, and if this is even the right answer, or if I'm just getting lucky.
Remove the data annotations and try with these configurations:
builder.Entity<UserSettings>()
.Property(b => b.UserId)
.HasColumnName("User_Id_Fk");
builder.Entity<User>()
.HasOne(us => us.UserSettings)
.WithOne(u => u.User)
.HasForeignKey<UserSettings>(b => b.UserId);
From EF Core documentation:
When configuring the foreign key you need to specify the dependent
entity type - notice the generic parameter provided to HasForeignKey
in the listing above. In a one-to-many relationship it is clear that
the entity with the reference navigation is the dependent and the one
with the collection is the principal. But this is not so in a
one-to-one relationship - hence the need to explicitly define it.
The example that is presented in the quoted link (Blog-BlogImage) is pretty much the same of what are you trying to achieve.
If the solution that I show above doesn't work, then you should check if User_Id_Fk column allows null. If that is the case, change the FK property type to int?:
public class UserSettings
{
public int Id { get; set; }
public int? UserId { get; set; }
public virtual User User { get; set; }
}

Unidirectional One to One relationship entity framework, cascade delete doesn't work

I want to implement a unidirectional one to one relationship; however on cascade delete doesn't work.
I have the following classes:
public class Student
{
public int Id { get; set; }
public string Name { get; set; }
public Address Address { get; set; }
}
public class Address
{
public int Id { get; set; }
public string Street { get; set; }
//I don't want the StudentId foreign key or the property of Student class here
}
In my Context class, I'm mapping the relationship like this:
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<Student>()
.HasRequired(s => s.Address)
.WithOptional()
.Map(m => m.MapKey("Address_Id"))
.WillCascadeOnDelete();
}
For some reason, it's not deleting the address when, student object is deleted.
Moreover, I also want to add the foreign key property (i.e. AddressId) in the Student class like this:
[ForeignKey("Address")]
[Column("Address_Id")]
public string AddressId { get; set; }
However, I get this error when I try to add a new migration:
Address_Id: Name: Each property name in a type must be unique. Property name 'Address_Id' is already defined.
I do believe I'm mixing things up (with MapKey and the attributes of AddressId). However, I don't know how to fix this.
I went through this SO question and this article; however, no luck so far.
Link to DotNetFiddle. It won't work cause there is no database.
For some reason, it's not deleting the address when, student object is deleted.
That's the normal behavior for the relationship you have defined. It's not a matter of data annotations or fluent configuration. If you have different expectations, you'd better revisit your model.
Every relationship has a one side called principal and another side called dependent. The principal side (a.k.a. master, primary) is the one being referenced. The dependent side (a.k.a. detail, secondary) is the one that is referencing the principal. The foreign key is put on the dependent side and must always point to an existing principal or null when the relationship is optional. The cascade delete works by deleing all the dependent records when the principal record is deleted.
As explained in the How Code First Determines the Principal and Dependent Ends in an Association? section of the article mentioned by you, EF always uses the required side as principal and allows you to chose the one only when both are required.
With all that being said, let see what you have.
Address is required, Student is optional. Also you want to put FK in Student, i.e. Student references Address.
All that means that in your relationship, Address is the principal and Student is the dependent. Which means the Address may exists w/o Student referencing it. If cascade delete is turned on (as you did), deleting the Address will delete the Student, not the opposite.
I think all that should explain why it's is working the way it is now, and that no attributes or configuration can help to achieve what are you asking for. If you want it differently, the same article (and related from the same series) explains how to configure the relationship to either use Shared Primary Key Association or Foreign Key Association at the Address side. Whether it is unidirectional or bidirectional absolutely has nothing in common with the problem - see Should We Make the Associations Bidirectional? section in the article.
You foreign key should be like this :
public class Student
{
public int Id { get; set; }
public string Name { get; set; }
[ForeignKey("AddressId")]
public Address Address { get; set; }
[Column("Address_Id")]
public int AddressId { get; set; }
}
In your fluent mapping you just need:
modelBuilder.Entity<Student>()
.HasRequired(s => s.Address)
.WillCascadeOnDelete(true);
Or you can force cascade delete with an annotation:
[Required]
[ForeignKey("AddressId")]
public Address Address { get; set; }
Now update your database and your mapping should be correct and the delete should cascade.

Why is my Seed() method not saving and connecting entities correctly?

I am building a registration site for a conference for my organization, with multiple VIPs and guest speakers. The requirement is to track many details about each attendee including their arrival and departure plans and their local lodging information. In order to facilitate discussion with stakeholders on the types of reports we need to build, I want to populate my dev database with a batch of records from a CSV containing randomly generated information like name, arrival/departure date/time, etc. This will allow us to look at a working site without having to register and re-register many times.
However, I simply cannot get the Seed method to persist the relevant records properly, yet my controller which handles the registration works perfectly.
My database structure is basically an Attendee entity with child entities for TravelSchedule, LodgingArrangement, and various lookups. Here are excerpts from my entities:
public class Attendee
{
public int Id { get; set; }
... other strings/etc ...
public virtual TravelSchedule TravelSchedule { get; set; }
public int TravelScheduleId { get; set; }
public virtual LodgingArrangment LodgingArrangement { get; set; }
public int LodgingArrangementId { get; set; }
}
public class TravelSchedule
{
public int Id { get; set; }
... other properties ...
public virtual Attendee Attendee { get; set; }
public int AttendeeId { get; set; }
}
public class LodgingArrangement
{
public int Id { get; set; }
... other properties ...
public virtual Attendee Attendee { get; set; }
public int AttendeeId { get; set; }
}
Here is the content of my context's OnModelCreating method:
modelBuilder.Entity<Attendee>()
.HasOptional(a => a.TravelSchedule)
.WithRequired(r => r.Attendee);
modelBuilder.Entity<TravelSchedule>()
.HasRequired(m => m.ArrivalMode)
.WithMany(m => m.Arrivals)
.HasForeignKey(m => m.ArrivalModeId)
.WillCascadeOnDelete(false);
modelBuilder.Entity<TravelSchedule>()
.HasRequired(m => m.DepartureMode)
.WithMany(m => m.Departures)
.HasForeignKey(m => m.DepartureModeId)
.WillCascadeOnDelete(false);
modelBuilder.Entity<Attendee>()
.HasOptional(a => a.LodgingArrangement)
.WithRequired(l => l.Attendee);
The following is an excerpt from my Seed method.
var attendees = GetAttendeesFromCsv();
context.Attendees.AddOrUpdate(a => a.Email, attendees.ToArray());
context.SaveChanges();
var dbAttendees = context.Attendees.ToList();
foreach (var attendee in dbAttendees)
{
attendee.TravelSchedule = CreateTravelSchedule();
context.Entry<Attendee>(attendee).State = EntityState.Modified;
context.SaveChanges();
}
GetAttendeesFromCsv() extracts the records from the CSV into Attendee objects, thanks to the CsvHelper package. CreateTravelSchedule creates a new TravelSchedule entity and populates it with data from lookup tables using the SelectRandom() method from extensionmethod.com. The bottom line is that I extract the CSV rows into Attendee objects, add a new randomly-generated TravelSchedule entity, and save the resulting Attendee with attached TravelSchedule.
Except this does not work. Instead the above code adds the TravelSchedule records to the database, but the AttendeeId is always set to 0 in the table. Also the Attendees table is populated with all of the records from the CSV, but the TravelScheduleId on each row is always 0 as well. However, when stepping through the update-database call with the debugger the attendee.Id is populated properly, so by my understanding EF should pick up that the two are related and persist the related TravelSchedule at the same time as the Attendee. So why isn't EF connecting the two records?
Changing the loop to this:
foreach (var attendee in dbAttendees)
{
var travel = CreateTravelSchedule();
travel.AttendeeId = attendee.Id; // I also tried just travel.Attendee = attendee, without success
context.TravelSchedules.Add(travel);
context.SaveChanges();
}
Results in this error:
System.Data.Entity.Core.UpdateException: An error occurred while updating the entries. See the inner exception for details. ---> System.Data.SqlClient.SqlException: The INSERT statement conflicted with the FOREIGN KEY constraint "FK_dbo.TravelSchedules_dbo.Attendees_Id". The conflict occurred in database "MySite.DAL.MyContext", table "dbo.Attendees", column 'Id'.
So it appears I cannot add the TravelSchedule entity to the Attendee, and I also cannot go "backwards" by creating the TravelSchedule and then attaching the Attendee.
The frustrating part is that the registration form logic in my controller works perfectly fine, excerpt below. The walkthrough is that the registration controller stores each screen's data (view models) in the session using a static WorkflowManager class which handles persistence between screens. After user confirmation the controller pulls each screen's details from the WorkflowManager, runs them through AutoMapper to convert them to the relevant populated DAL entities, attaches those entities to the attendee entity, and saves it all to the database.
Again, this works perfectly fine, saving the attendee and its two child entities without error. Here is the relevant excerpt of the controller action:
var attendeeRegistration = WorkflowManager.GetAttendeeRegistration();
var travelRegistration = WorkflowManager.GetTravelRegistration();
using (var db = new MyContext())
{
var attendee = Mapper.Map<Attendee>(attendeeRegistration);
attendee.AnotherChildEntity = db.ChildEntities.Find(attendeeRegistration.SelectedChildEntityId);
var travel = Mapper.Map<TravelSchedule>(travelRegistration);
travel.ArrivalMode = db.TravelModes.Find(travelRegistration.SelectedArrivalModeId);
travel.DepartureMode = db.TravelModes.Find(travelRegistration.SelectedDepartureModeId);
var lodging = Mapper.Map<LodgingArrangement>(lodgingRegistration);
lodging.LodgingLocation = db.LodgingLocations.Find(lodgingRegistration.SelectedLodgingLocationId);
attendee.Comments = comments;
attendee.TravelSchedule = travel;
attendee.LodgingArrangement = lodging;
db.Attendees.Add(attendee);
db.SaveChanges();
}
This works perfectly. None of these objects are in the database until after the user confirms the registration is complete. So I don't understand why I can persist new entities to the database here, yet I can't do what appears to me to be the same thing in the Seed method above.
Any help or ideas much appreciated. This has been causing me endless grief for days now. Thanks.
The association between Attendee and TravelSchedule is 1:1. EF implements 1:1 associations by creating a primary key in the dependent entity (here: TravelSchedule) that's also a foreign key to its principal entity (Attendee).
So Attendee.TravelScheduleId and TravelSchedule.AttendeeId are not used for the association and their values remain 0. Which means: the Seed works without these fields/properties (and I'd even expect it to work with them), it establishes the associations through the Id fields.

Categories

Resources