Linq mystery error in EF query - c#

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.

Related

EF Core 7 - how to set up multiple nullable GUID foreign keys on child object without triggering foreign key conflict

I have 3 entities: Person, User, and Location.
A Person can have multiple Locations
A User can have multiple Locations
My entities are set up as such:
public class Person
{
public Guid Id { get; set; }
public string Name { get; set; }
public virtual IList<Location>? Locations { get; set; }
}
public class PersonEntityTypeConfiguration : IEntityTypeConfiguration<Person>
{
public void Configure(EntityTypeBuilder<Person> builder)
{
builder
.HasMany(b => b.Locations)
.WithOne(b => b.Person)
.HasForeignKey(b => b.PersonId)
.IsRequired(false);
}
}
public class User
{
public Guid Id { get; set; }
public Guid? Username { get; set; }
public virtual IList<Location>? Locations { get; set; }
}
public class UserEntityTypeConfiguration : IEntityTypeConfiguration<User>
{
public void Configure(EntityTypeBuilder<User> builder)
{
builder
.HasMany(b => b.Locations)
.WithOne(b => b.User)
.HasForeignKey(b => b.UserId)
.IsRequired(false);
}
}
public class Location : UdbObject
{
public Guid Id { get; set; }
public string Description { get; set; }
[ForeignKey(nameof(Person))]
public Guid? PersonId { get; set; }
public virtual Person? Person { get; set; }
[ForeignKey(nameof(User))]
public Guid? UserId { get; set; }
public virtual User? User { get; set; }
}
Problem: I tried to insert a User into my SQL Server DB. This user has one Location object within its IList<Location>? Locations collection. I am getting the following error: The INSERT statement conflicted with the FOREIGN KEY constraint "FK_Locations_Persons_PersonId".
Here is where it is going wrong:
Since Person.Id is a Guid? object, it automatically gets set to the equivalent of Guid.Empty before it is submitted to the DB. This causes the FK conflict, since the DB sees that there is no Person object in the DB with an Id set to the equivalent of Guid.Empty.
What I've tried: I saw that in previous version of EF Core, there is a .WithOptional() method that can be used in the Fluent API, but unfortunately this method is not recognized in EF Core 7. I tried to use the .IsRequired(false) method in the API, and it probably works from the DB standpoint, but my problem is that the GUID-based Id property is being set to Guid.Empty on the server before being passed to the DB, so .IsRequired(false) doesn't have the opportunity to do its job.
Am I missing something? Is there some other way to configure this?
Solution: I had a PersonDto that had a property public Guid Id { get; set; } instead of Guid? and it was being mapped back to the Person object with Guid.Empty loaded in it. Duh.
Just make them M2M relationships and the foreign keys will all be in bridge tables. eg
public class Location : UdbObject
{
public Guid Id { get; set; }
public string Description { get; set; }
public virtual ICollection<Person> Persons { get; } = new HashSet<Person>();
public virtual ICollection<User> Users { get; } = new HashSet<User>();
}

EF Core Invalid Column Name [TableNameColumn] with no relationships

I'm converting an EF project to EF Core.
I get an error 'Invalid Column Name' when performing a query that uses an .Include.
Simplified code:
[Table("Person")]
public class Person
{
[Key]
public Guid PersonId { get; set; }
public string Name { get; set; }
}
[Table("PersonExtra")]
public class PersonExtra
{
[Key]
public Guid PersonId { get; set; }
public string ExtraDetails { get; set; }
public ICollection<PersonCategory> Categories { get; set; }
}
[Table("PersonCategory")]
public class PersonCategory
{
public Guid PersonId { get; set; }
public int PersonCategoryId { get; set; }
[ForeignKey("PersonCategoryId")]
public PersonCategory PersonCategory { get; set; }
}
protected override void OnModelCreating(ModelBuilder builder)
{
builder.Entity<PersonCategory>().HasKey(x => new {
x.PersonId,
x.PersonCategoryId
});
}
Code that throws the error:
var dbEntity = await Context.PersonExtra
// this line blows it up
.Include(_ => _.Categories)
.FirstAsync(x => x.PersonId == id);
The one unusual thing here is PersonExtra is optional and has the same key as Person as it's basically an optional table extension of Person.
Originally Person had this navigation:
[Table("Person")]
public class Person
{
[Key]
public Guid PersonId { get; set; }
public string Name { get; set; }
[ForeignKey("PersonId")]
public PersonExtra PersonExtra { get; set; }
}
but I still get the error when I remove the relationship completely from the code.
Full error is:
Invalid column name 'PersonExtraPersonId'.
I have tried configuring the relationship in the model builder. It makes no difference. If I do it both ways via attributes I get:
A relationship cycle involving the primary keys of the following entity types was detected
I suspect this error is caused somewhere else but there is no other references to PersonExtra so I'm at a loss what to look for.

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

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; }
}

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.

Relationship troubles with Entity Framework

I need help creating the relationship in entity framework as everything I have tried gives me errors when trying to add the migration or if I get passed that then I try to update the database and get an error about indexes with the same name.
public class Profile
{
public Profile()
{
Environments = new HashSet<Environment>();
}
[Key]
public int Id { get; set; }
public string VersionCreated { get; set; }
public string DiskLocation { get; set; }
public string Name { get; set; }
public DateTime DateTime { get; set; }
public virtual Product Product { get; set; }
public virtual Instance OriginalInstance { get; set; }
public virtual ICollection<Environment> Environments { get; set; }
}
public class Instance
{
public Instance()
{
TestResults = new HashSet<TestResult>();
Environments = new HashSet<Environment>();
}
[Key]
public int Id { get; set; }
public string Name { get; set; }
public string Version { get; set; }
public string UserFriendlyName { get; set; }
public virtual Product Product { get; set; }
public virtual Profile LastKnownProfile { get; set; }
public virtual Computer Computer { get; set; }
public virtual ICollection<TestResult> TestResults { get; set; }
public virtual ICollection<Environment> Environments { get; set; }
}
The problem with the above classes is that the OrginalInstance property on the Profile class and the LastKnownProfile in the Instance class are supposed to just be foreign keys to those specific tables and they probably won't be the same very often. They can also both possibly be null.
I have tried:
modelBuilder.Entity<Instance>().HasRequired(i => i.LastKnownProfile);
modelBuilder.Entity<Profile>().HasRequired(p => p.OriginalInstance);
This gave me an Unable to determine the principal end of an association between the types 'EcuWeb.Data.Entities.Instance' and 'EcuWeb.Data.Entities.Profile'. The principal end of this association must be explicitly configured using either the relationship fluent API or data annotations. error.
and with:
modelBuilder.Entity<Instance>().HasRequired(i => i.LastKnownProfile).WithOptional();
modelBuilder.Entity<Profile>().HasRequired(p => p.OriginalInstance).WithOptional();
The database adds a foreign key reference back to itself.
...that the OrginalInstance property on the Profile class and the
LastKnownProfile in the Instance class are supposed to just be foreign
keys to those specific tables and they probably won't be the same very
often. They can also both possibly be null.
In this case you actually want two one-to-many relationships between Profile and Instance if I don't misunderstand your quote above. It would mean that many Profiles can have the same OriginalInstance and that many Instances can have the same LastKnownProfile. The correct mapping would look like this then:
modelBuilder.Entity<Profile>()
.HasOptional(p => p.OriginalInstance)
.WithMany()
.Map(m => m.MapKey("OriginalInstanceId"));
modelBuilder.Entity<Instance>()
.HasOptional(i => i.LastKnownProfile)
.WithMany()
.Map(m => m.MapKey("LastKnownProfileId"));
The lines with MapKey are optional. Without them EF will create a foreign key with a default name.
Also note that you must use HasOptional (instead of HasRequired) if "both can possibly be null".

Categories