Related many to one navigation properties not loading with EF code first - c#

I am struggling to get some related navigation properties loading up using EF code first.
I have a User table which is my primary table, which every time you update it, it generates a UserLight object containing only the basics. This is linked via a one-to-one mapping with User, so the User object generates the identity key and then when it saves, a UserLlight object is created using that UserId as it's key.
I now have a conversation object between two users which I want to load in only the UserLight objects for the sender and receiver, for performance reasons. I have tried mapping using Fluent and CF but when I load the objects from my repository, only the UserStartedId and UserRecipientId integer fields are populated, the actual UserLight objects UserStarted and UserRecipient are null.
My conversation class is as follows
public class DbConversation
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public long ConversationId { get; set; }
public virtual DbUserLight UserStarted { get; set; }
public int UserStartedId { get; set; }
public virtual DbUserLight UserRecipient { get; set; }
public int UserRecipientId { get; set; }
public virtual IList<DbStoryMessage> Messages { get; set; }
}
My Userlight class is as follows (abbreviated)
public class DbUserLight
{
public int UserId { get; set; }
[Required]
[MaxLength(50)]
public string FirstName { get; set; }
[Required]
public DateTime DateOfBirth { get; set; }
}
My DbContext OnModelCreating has the following
modelBuilder.Entity<DbUserLight>()
.HasKey(a => a.UserId);
modelBuilder.Entity<DbUserLight>()
.Property(a => a.UserId)
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.None);
modelBuilder.Entity<DbStoryConversation>()
.HasKey(c => c.ConversationId);
modelBuilder.Entity<DbStoryConversation>()
.Property(c => c.ConversationId)
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
modelBuilder.Entity<DbStoryConversation>()
.HasMany<DbStoryMessage>(c => c.Messages)
.WithRequired(m => m.Conversation)
.WillCascadeOnDelete(true);
And my repository call is as follows
public IQueryable<DbStoryInboxMessage> GetInboxMessages()
{
return Work.Context.StoryInboxMessages
.Include(i => i.Conversation.UserStarted)
.Include(i => i.Conversation.UserRecipient)
.Include(i => i.Conversation.Messages);
}
Can anyone shed any light on why this is not working?

Did you try adding the foreign key annotations?
public class DbConversation
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public long ConversationId { get; set; }
[ForeignKey("UserStartedId")]
public virtual DbUserLight UserStarted { get; set; }
public int UserStartedId { get; set; }
[ForeignKey("UserRecipientId")]
public virtual DbUserLight UserRecipient { get; set; }
public int UserRecipientId { get; set; }
public virtual IList<DbStoryMessage> Messages { get; set; }
}

Related

How to code C# MVC model for many to many join table for sqlite database migration { edited }

I am creating a sqlite database to track users assigned to teams. This would mean many-to-many table relationship. I need help making the model for the migration.
users table:
public class User
{
public int Id { get; set; }
public string Username { get; set; }
public ICollection<Photo> Photos { get; set; }
public virtual ICollection<UserTeam> UserTeams { get; set; }
}
team table:
public class Team
{
public int Id { get; set; }
public string Name { get; set; }
public User User { get; set; }
public int UserId { get; set; }
public virtual ICollection<UserTeam> UserTeams{ get; set; }
}
UserTeam Table:
public class UserTeam
{
public User User { get; set; }
public int UserId{ get; set; }
public Team Team{ get; set; }
public int TeamId{ get; set; } //... I have this one for owner of the team
public string Title { get; set; }
}
DataContext:
public class DataContext : DbContext
{
public DataContext(DbContextOptions<DataContext> options) : base (options) {}
public DbSet<Value> Values { get; set; }
public DbSet<User> Users { get; set; }
public DbSet<Photo> Photos { get; set; }
public DbSet<Team> Teams { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<UserTeam>()
.HasKey(ut => new { ut.UserId, ut.TeamId });
modelBuilder.Entity<UserTeam>()
.HasOne(ut => ut.User)
.WithMany(u => u.UserTeams)
.HasForeignKey(ut => ut.UserId);
modelBuilder.Entity<UserTeam>()
.HasOne(ut => ut.Team)
.WithMany(t => t.UserTeams)
.HasForeignKey(ut => ut.TeamId);
}
}
From what I read declaring I ICollection on team class and user class would force a join table creating. However when I try to migrate, I get this message: Unable to determine the relationship represented by navigation property 'User.Teams' of type 'ICollection'. Either manually configure the relationship, or ignore this property using the '[NotMapped]' attribute or by using 'EntityTypeBuilder.Ignore' in 'OnModelCreating'.
Any suggestions on how to correctly make join tables in sqlite would be helpful
looks like you are using EF Core 2.x. It is necessary to define the entity in the model. This means you will have to define relationship by overriding it in the OnModelCreating method.
I would further normalize your structure by creating another table to hold the reference for the user and the team the user belongs to and vice-versa. I would call this table UserTeam (for the lack of better naming)
So I will have something like:
public class User
{
//....omitted
public virtual ICollection<UserTeam> UserTeams { get; set; }
}
public class UserTeam
{
public int UserId{ get; set; }
public User User { get; set; }
public int TeamId{ get; set; }
public Team Team{ get; set; }
}
public class Team
{
//.... omitted
public virtual ICollection<UserTeam> UserTeams{ get; set; }
}
The UserTeam table will need to be configured so that EFCore can map it successfully. This is where we define the many-to-many relationship
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<UserTeam>()
.HasKey(ut => new { ut.UserId, ut.TeamId });
modelBuilder.Entity<UserTeam>()
.HasOne(ut => ut.User)
.WithMany(u => u.UserTeams)
.HasForeignKey(ut => ut.UserId);
modelBuilder.Entity<UserTeam>()
.HasOne(ut => ut.Team)
.WithMany(t => t.UserTeams)
.HasForeignKey(ut => ut.TeamId);
}

Is it possible to set up a navigation property without the foreign key?

Is it possible to have two (or more) navigation properties of the same type?
My model looks like this...
public class Agreement
{
public int Id { get; set; }
public Guid? BuyerId { get; set; }
public Guid? SellerId { get; set; }
public AgreementInfo ByerAgreementInfo { get; set; }
public AgreementInfo SellerAgreementInfo { get; set; }
}
public class AgreementInfo
{
// PK is AgreementId and OwnerActorId combined.
public int AgreementId { get; set; }
public Guid OwnerActorId { get; set; }
public string Name { get; set; }
}
... and i'm trying to include the navigation properties by matching the AgreementId and the ByerId/SellerId...
modelBuilder.Entity<Agreement>().HasOne(x => x.ByerAgreementInfo).WithOne().HasForeignKey<Agreement>(x => new {x.Id, x.ProviderId});
modelBuilder.Entity<Agreement>().HasOne(x => x.SellerAgreementInfo).WithOne().HasForeignKey<Agreement>(x => new { x.Id, x.RequesterId });
... but this results in a circural dependecy.
Is there any way to include these properties without using a foreign key? Or is there another solution (except for adding an id-column to the info table) that allows me to use the info-table rows as navigation properties in the agreement class?
... but this results in a circural dependecy
Yes! It will. To overcome this you have to specify .OnDelete(DeleteBehavior.Restrict); in your Fluent API configuration as follows but first you have to write your Agreement model class as follows too:
public class Agreement
{
public int Id { get; set; }
public Guid? BuyerId { get; set; }
public Guid? SellerId { get; set; }
public int AgreementIdForBuyer { get; set; }
public Guid OwnerActorIdForBuyer { get; set; }
public int AgreementIdForSeller { get; set; }
public Guid OwnerActorIdForSeller { get; set; }
public AgreementInfo ByerAgreementInfo { get; set; }
public AgreementInfo SellerAgreementInfo { get; set; }
}
Now in Fluent API configuration:
modelBuilder.Entity<Agreement>()
.HasOne(x => x.ByerAgreementInfo)
.WithOne()
.HasForeignKey<Agreement>(p => new {p.AgreementIdForBuyer, p.AgreementIdForBuyer})
.OnDelete(DeleteBehavior.Restrict); // <-- Here it is
modelBuilder.Entity<Agreement>()
.HasOne(x => x.SellerAgreementInfo)
.WithOne()
.HasForeignKey<Agreement>(p => new {p.AgreementIdForSeller, p.OwnerActorIdForSeller})
.OnDelete(DeleteBehavior.Restrict); // <-- Here it is
There are more than a handful of ways to achieve what you want. You could use Data Annotations with InverseProperty attribute on your child, ForeignKey attribute on your parent, or fluent syntax either way in your model builder. I tend to use attributes (Data Annotations) where I can (it's just my personal taste), I find it need to see the relationships in the actual mode itself (though others might not).
Using ForeignKey attribute:
public class Agreement
{
...
[ForeignKey("ByerAgreementInfo ")]
public int ByerAgreementInfoId { get; set; }
public AgreementInfo ByerAgreementInfo { get; set; }
[ForeignKey("SellerAgreementInfo ")]
public int SellerAgreementInfoId { get; set; }
public AgreementInfo SellerAgreementInfo { get; set;
}
Using InverseProperty attribute:
public class AgreementInfo
{
...
[InverseProperty("ByerAgreementInfo ")]
public ICollection<Agreement> Sellers { get; set; }
[InverseProperty("SellerAgreementInfo ")]
public ICollection<Agreement> Buyers { get; set; }
}
If you want to use Fluent Syntax, I believe the following will work (though I haven't used it for a while):
modelBuilder.Entity<Agreement>()
.HasOne(x => x.ByerAgreementInfo)
.WithOne()
.HasForeignKey<Agreement>(p => p.ByerAgreementInfoId);
modelBuilder.Entity<Agreement>()
.HasOne(x => x.SellerAgreementInfo)
.WithOne()
.HasForeignKey<Agreement>(p => p.SellerAgreementInfoId);
Note: I'm think you may need the actual Id's in your model, though I can't remember.

EF: Multiple 1-to-1 or zero on same parent table

I have a problem similar to Entity Framework Code First : Setting up One-To-One foreign key association using Annotations
However, I have many "optional" references on the same parent table to same child table.
If I call classes CE and CA, to simplify, CE has a number of CA fields. CA id's, then, cannot have as key only the same id as CE.
What I want is EF to recognize CA/id as primary key and ConfiguracionEmpresaId as the foreign key relationship. Simply adding it to the keys didn't work.
I tried many things and I think is time to ask for help.
Simplified code:
public class ConfiguracionEmpresa
{
[Key]
[ForeignKey("Empresa")]
public long Id { get; set; }
public virtual Empresa Empresa { get; set; }
[StringLength(200)]
public string DirectorioTrabajo { get; set; }
public virtual ConfiguracionArchivo Cfg_LibroMayor { get; set; }
public virtual ConfiguracionArchivo Cfg_LibroDiario { get; set; }
public virtual ConfiguracionArchivo Cfg_Balance { get; set; }
public virtual ConfiguracionArchivo Cfg_DiccionarioCuentas { get; set; }
}
public class ConfiguracionArchivo
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public long Id { get; set; }
public long ConfiguracionEmpresaId { get; set; }
[Required, ForeignKey("ConfiguracionEmpresaId")]
public virtual ConfiguracionEmpresa ConfiguracionEmpresa { get; set; }
public int LineasCabecera { get; set; }
}
The problem is I cannot have the same parent ID on child table, it will have duplicated primary keys.
On ModelBuilder:
modelBuilder.Entity<ConfiguracionArchivo>().HasKey(e => e.Id).HasRequired(e => e.ConfiguracionEmpresa);
modelBuilder.Entity<ConfiguracionEmpresa>()
.HasOptional(d => d.Cfg_Balance).WithRequired(q => q.ConfiguracionEmpresa);
modelBuilder.Entity<ConfiguracionEmpresa>()
.HasOptional(d => d.Cfg_DiccionarioCuentas).WithRequired(q => q.ConfiguracionEmpresa);
modelBuilder.Entity<ConfiguracionEmpresa>()
.HasOptional(d => d.Cfg_LibroDiario).WithRequired(q => q.ConfiguracionEmpresa);
modelBuilder.Entity<ConfiguracionEmpresa>()
.HasOptional(d => d.Cfg_LibroMayor).WithRequired(q => q.ConfiguracionEmpresa);

Linq mystery error in EF query

I have a UserProfile class
[Key]
public int UserProfileId { get; set; }
public string AppUserId { get; set; }
...code removed for brevity
[Required]
public NotificationMethod NotificationMethod { get; set; }
public List<PrivateMessage> PrivateMessages { get; set; }
public List<Machine> OwnedMachines { get; set; }
public bool IsProfileComplete { get; set; }
public byte[] Avatar { get; set; }
public string AvatarUrl { get; set; }
public string GetFullName()
{
return $"{FirstName} {LastName}";
}
}
I also have a PrivateMessage class
public class PrivateMessage
{
[Key]
public int Id { get; set; }
public int MessageToUserId { get; set; }
public int MessageFromUserId { get; set; }
public DateTime DateSent { get; set; }
public string Message { get; set; }
}
I set up a simple test to pull the user profile out with various includes. The PrivateMessages always errors. Here is a sample method that errors.
public static UserProfile GetUserProfileIncluding(string appUserId)
{
using (RestorationContext)
{
//RestorationContext.Database.Log = s => System.Diagnostics.Debug.WriteLine(s);
return RestorationContext.MemberProfiles.Where(m => m.AppUserId == appUserId)
.Include(m=> m.PrivateMessages)
.FirstOrDefault();
}
}
The error noted is
InnerException {"Invalid column name 'UserProfile_UserProfileId'.\r\nInvalid column name 'UserProfile_UserProfileId'."} System.Exception {System.Data.SqlClient.SqlException}
Which I don't understand, neither table has a column "UserProfile_UserProfileId"
If I use the property OwnedMachines instead of PrivateMessages, it works perfectly fine (well not really, its only pulling in 4 records when there are 6 that match but I can figure that out later).
public static UserProfile GetUserProfileIncluding(string appUserId)
{
using (RestorationContext)
{
return RestorationContext.MemberProfiles.Where(m => m.AppUserId == appUserId)
.Include(m=> m.OwnedMachines)
.FirstOrDefault();
}
}
And you can see below, Machine is set up exactly like PrivateMessage, albeit it has two UserProfiles instead of one
public class Machine
{
[Key]
public int MachineId { get; set; }
public int OwnerProfileId { get; set; }
public int SerialNumber { get; set; }
public string YearofManufacture { get; set; }
public string ModelName { get; set; }
public Manufacturer Manufacturer { get; set; }
public DateTime DateAcquired { get; set; }
}
I've spent far to much time on this now. Does it have something to do with the fact that I have two UserProfile Id int properties in PrivateMessage? (MessageToUserId & MessageFromUserId). I originally had these set as foreign keys with a UserProfile property in there as well like this
[ForeignKey("MessageToProfile")]
public int MessageToUserId { get; set; }
public UserProfile MessageToProfile { get; set; }
[ForeignKey("MessageFromProfile")]
public int MessageFromUserId { get; set; }
public UserProfile MessageFromProfile { get; set; }
But I removed them thinking they may have been the source of the error, but apparently not.
UPDATE:
After a bunch more trial and error, it is apparent that the current method will always err as the method is looking for a navigable property which doesn't exist. Since I have the two int properties in PrivateMessage, when trying to include those in the UserProfile object, I will need to filter then by MessageToUserId and then include them. Not sure how to filter and include.
Using this method should work;
public static UserProfile GetProfileForLoggedInUser(string appUserId)
{
using (RestorationContext)
{
RestorationContext.Database.Log = s => System.Diagnostics.Debug.WriteLine(s);
var profile= RestorationContext.MemberProfiles.Include(m => m.OwnedMachines)
.FirstOrDefault(m => m.AppUserId == appUserId);
var pms = RestorationContext.PrivateMessages.Where(m => m.MessageToUserId == profile.UserProfileId).ToList();
if (profile != null) profile.PrivateMessages = pms;
return profile;
}
}
But it gives the same invalid column error UserProfile_UserProfileID.
Here is the TSql
SELECT
[Extent1].[Id] AS [Id],
[Extent1].[MessageToUserId] AS [MessageToUserId],
[Extent1].[MessageFromUserId] AS [MessageFromUserId],
[Extent1].[DateSent] AS [DateSent],
[Extent1].[Message] AS [Message],
[Extent1].[UserProfile_UserProfileId] AS [UserProfile_UserProfileId]
FROM [RestorationContext].[PrivateMessages] AS [Extent1]
WHERE [Extent1].[MessageToUserId] = #p__linq__0
Since this is just querying the PrivateMessage table WHY is it looking for that UserProfileId, it has nothing to do with this table. Here are the table properties from SSMS
Where is that UserProfileID crap coming from?
Your Machine inclusion works because the Machine class has only one foreign key of UserProfile.
You have 2 foreign keys to the same table in PrivateMessage class, naturally, you would need 2 ICollection navigation properties in your UserProfile class. EntityFramework didn't know which foreign key to use in your PrivateMessage class for loading your ICollection<PrivateMessage> property in your UserProfile class.
public ICollection<PrivateMessage> FromPrivateMessages { get; set; }
public ICollection<PrivateMessage> ToPrivateMessages { get; set; }
In your context class
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<PrivateMessage>()
.HasRequired(m => m.MessageFromProfile)
.WithMany(t => t.FromPrivateMessages)
.HasForeignKey(m => m.MessageFromUserId)
.WillCascadeOnDelete(false);
modelBuilder.Entity<PrivateMessage>()
.HasRequired(m => m.MessageToProfile)
.WithMany(t => t.ToPrivateMessages)
.HasForeignKey(m => m.MessageToUserId)
.WillCascadeOnDelete(false);
}
UPDATE
EF uses convention over configuration, and by having navigation properties of UserProfile in your PrivateMessage class will imply a relationship and EF will try to find a foreign key in the form of <Navigation Property Name>_<Primary Key Name of Navigation Property type>, which gives you UserProfile_UserProfileId.
You should be wondering why UserProfile_UserProfileId instead of UserProfile_MessageToUserId or UserProfile_MessageFromUserId at this point. That's because of your foreign key attribute, telling EF to use the UserProfileId property in your UserProfile class.
What you can do now is, remove the foreign key attributes like this
public int MessageToUserId { get; set; }
public UserProfile MessageToProfile { get; set; }
public int MessageFromUserId { get; set; }
public UserProfile MessageFromProfile {get; set; }
and add another ICollection and do the modelBuilder configuration like how I stated before the update.

Entity Framework with Condition Relationship

Is it possible to have a relationship that is based on a condition in Entity Framework? My model looks something like this...
public class Document
{
public int Id { get; set; }
public string Name { get; set; }
public OwnerType OwnerType { get; set; }
public int OwnerId { get; set; }
public virtual Organization OrganizationOwner { get; set; }
public virtual User UserOwner { get; set; }
}
public enum OwnerType
{
Organization = 1,
User = 2
}
public class Organization
{
public int Id { get; set; }
public string Name { get; set; }
//[other properties specific to Organization]
public virtual List<Documents> Documents { get; set; }
}
public class User
{
public int Id { get; set; }
public string Name { get; set; }
//[other properties specific to User]
public virtual List<Documents> Documents { get; set; }
}
So, what I'd like to is set up a relationship so that the OrganizationOwner property of a Document instance automatically gets populated when the OwnerType == OwnerType.Organization, and the UserOwner property is populated when OwnerType == OwnerType.User.
Is this possible to set up this kind of relationship in EntityFramework - Code First? Something like this in the mapping...
EntityTypeConfiguration<Document>.HasOptional(d => d.OrganizationOwner)
.WithMany(o => o.Documents)
.HasForeignKey(d => d.OwnerId)
.Where(d => d.OwnerType == OwnerType.Organization);
EntityTypeConfiguration<Document>.HasOptional(d => d.UserOwner)
.WithMany(u => u.Documents)
.HasForeignKey(d => d.OwnerId)
.Where(d => d.OwnerType == OwnerType.User);
I would like to be able to leverage joins on the OrganizationOwner and UserOwner when setting up my Linq queries on the context so that I don't have to do a separate selects on those entities for each Document. Is this type of relationship supported or is there a better way to do this? Thanks.

Categories