Difficulty Concerning EF Code First Fluent API, TPH, and Foreign Keys - c#

I have two tables in my database. One is called Users, and the other is called Widgets. The Widgets table represents 3 entities in my code model. One of the entities, Widget, is a parent class for the other two entities, WidgetTypeA and WidgetTypeB. Both WidgetTypeA and WidgetTypeB have navigation properties to the User entity, which is persisted to the Users table in the database. I'm having trouble getting Code First to use the same foreign key for both the WidgetTypeA and WidgetTypeB entities (UserId). Does anyone know how to do this? It seems like it should be a common problem with Table Per Hierarchy mapping.
My entity classes are as follows:
public class Widget
{
public int Id { get; set; }
public string Name { get; set; }
}
class WidgetMap : EntityTypeConfiguration<Widget>
{
public WidgetMap()
{
ToTable("Widgets");
HasKey(w => w.Id);
Property(w => w.Id)
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
Property(w => w.Name)
.IsRequired()
.HasMaxLength(75)
.IsUnicode(true);
}
}
public class WidgetTypeA : Widget
{
public int UserId { get; set; }
public virtual User User { get; set; }
public string Color { get; set; }
public int DepthLevel { get; set; }
}
class WidgetTypeAMap : EntityTypeConfiguration<WidgetTypeA>
{
public WidgetTypeAMap()
{
Map(w => w.Requires("WidgetTypeId").HasValue(1));
HasRequired(w => w.User)
.WithMany(u => u.WidgetTypeAs)
.HasForeignKey(w => w.UserId)
.WillCascadeOnDelete(false);
Property(w => w.Color)
.IsOptional()
.IsUnicode(true)
.HasMaxLength(75);
Property(w => w.DepthLevel)
.IsOptional();
}
}
public class WidgetTypeB : Widget
{
public int UserId { get; set; }
public virtual User User { get; set; }
}
class WidgetTypeBMap : EntityTypeConfiguration<WidgetTypeB>
{
public WidgetTypeBMap()
{
Map(w => w.Requires("WidgetTypeId").HasValue(2));
HasRequired(w => w.User)
.WithMany(u => u.WidgetTypeBs)
.HasForeignKey(w => w.UserId)
.WillCascadeOnDelete(false);
}
}
public class User
{
public int Id { get; set; }
public string Username { get; set; }
public int Age { get; set; }
public virtual ICollection<WidgetTypeA> WidgetTypeAs { get; set; }
public virtual ICollection<WidgetTypeB> WidgetTypeBs { get; set; }
}
class UserMap : EntityTypeConfiguration<User>
{
public UserMap()
{
ToTable("Users");
HasKey(u => u.Id);
Property(u => u.Username)
.IsRequired()
.HasMaxLength(75)
.IsUnicode(true);
Property(u => u.Age)
.IsRequired();
}
}
At any rate, I keep getting the error
Invalid column name 'UserId1'
when I try to perform the following operations:
using (var entities = new MyEntities())
{
User u = new User
{
Username = "Frank",
Age = 14
};
entities.Users.Add(u);
entities.SaveChanges();
WidgetTypeA wa1 = new WidgetTypeA
{
Name = "0SDF81",
UserId = u.Id,
DepthLevel = 6
};
entities.WidgetTypeAs.Add(wa1);
entities.SaveChanges();
}
Not sure if this can be fixed or not. I can always specify a second UserId foreign key for the Widgets table, but that seems pointless. Perhaps there's a way to do this using Fluent API?

You cannot map properties defined in different derived entities to the same column. That is limitation in EF. If your WidgetTypeA has UserId property and your WidgetTypeB has UserId property they must be different columns in the database. It should work if you move both UserId and User properties from derived types to the parent Widget type.

I know its a long way late, but hopefully may help other readers.
Although Ladislav was correct that using a mapped Foreign Key is not supported in EF6, I did find a useful workaround.
It is possible to define a computed column specification whose expression simply refers to the original column. Userid in the description above. This can be used as the discriminator for the TPH mapping. With this approach, the column need not be persisted, but can be used for TPH, with the original column being available for use as a foreign key.

Related

EF core tries to add a duplicated row to join table during entity update

I've researched a ton and couldn't find a solution that works for me.
The issue I have is the following:
I have a many to many relationship among my entities and I have a join table (without a "join model" in the code). And when trying to update an entity, EF Core tries to add a row to the join table, which already exists.
The thing is, that one of the tables is constant & is populated during migration and shouldn't be ever modified. The question is, how to tell EF Core, to not add any existing, valid rows to the second table & to the join table?
Here's the code - relationship configuration:
public class FirstModelsMap : IEntityTypeConfiguration<FirstModel>
{
public void Configure(EntityTypeBuilder<FirstModel> builder)
{
builder.HasKey(p => p.Id);
builder.HasIndex(nameof(FirstModel.InternalGuid));
builder.Property(p => p.Secret)
.HasMaxLength(128)
.IsRequired();
builder.Property(p => p.Name)
.IsRequired();
builder.Property(p => p.Url)
.IsRequired();
builder.Property(p => p.InternalGuid)
.IsRequired();
builder.HasMany(p => p.SecondModels)
.WithMany(p => p.FirstModels)
.UsingEntity(p => p.ToTable("FirstModelSecondModels"));
}
}
First entity:
public class FirstModel
{
public long Id { get; set; }
public Guid InternalGuid { get; set; }
public string Name { get; set; }
public Uri Url { get; set; }
public string Secret { get; set; }
public ICollection<SecondModel> SecondModels { get; set; }
}
Second entity:
public class SecondModel
{
public int Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public string InternalName { get; set; }
public ICollection<FirstModel> FirstModels { get; set; }
}
What I try to do in the repository:
public void Update(FirstModel firstModel)
{
firstModel.SecondModels = database.SecondModels.Where(e => firstModel.SecondModels.Select(e => e.Id).Contains(e.Id)).ToList();
// Tried adding this row, based on some StackOverflow answers
database.SecondModels.AttachRange(firstModel.SecondModels);
database.Update(firstModel);
}
What I get as a result when calling SaveChanges() in the service:
Microsoft.EntityFrameworkCore.DbUpdateException: 'An error occurred while saving the entity changes. See the inner exception for details.'
Inner Exception:SqlException: Violation of PRIMARY KEY constraint 'PK_FirstModelsSecondModels'. Cannot insert duplicate key in object 'dbo.FirstModelsSecondModels'. The duplicate key value is (1, 1).
My SecondModels table is being populated with migration & is not expected to be modified. The join table may be modified. However I want to let EF Core know, that some rows may already be in place. I want the join table to adjust based on the public ICollection<SecondModel> SecondModels { get; set; } value of the FirstModel. So that not relevant rows are deleted, existing rows are not added, new rows are added.
I'm sure I'm doing something wrong here (otherwise it would work). Can you please help?
I've ended up modifying the method the following way, which made everything work as expected, although I don't think this is the best solution by any means:
public async Task<bool> UpdateAsync(FirstModel firstModel)
{
var secondModelIds = firstModel.SecondModels.Select(e => e.Id);
var existingFirstModel = await database.FirstModel .Include(w => w.SecondModels).FirstOrDefaultAsync(w => firstModel.Id == w.Id);
if (existingFirstModel is null)
{
return false;
}
existingFirstModel.SecondModels= database.SecondModels.Where(e => secondModelIds .Contains(e.Id)).ToList();
existingFirstModel.InternalGuid = firstModel.InternalGuid;
existingFirstModel.Secret = firstModel.Secret;
existingFirstModel.Url = firstModel.Url;
existingFirstModel.Name = firstModel.Name;
// This is called in the service layer
await database.SaveChangesAsync()
return true;
}

Create one-to-many relationship by using Entity Framework Core database first

I have a .NET Core 3.1 library, which uses a database-first Entity Framework Core class.
To generate this class I use this package manager command:
Scaffold-DbContext “<my connection string>” Microsoft.EntityFrameworkCore.SqlServer
-OutputDir Database/GeneratedNew
-Context DatabaseContextBase -DataAnnotations
This worked for a long while, but now I am updating with more complexity and for a first time I am adding foreign keys.
In the database I have a table with primary key column ServerId. And another table (Currency) with the FK key column OriginServer.
They are both defined as numeric(20, 0).
This is the FK script, in case I messed it up:
ALTER TABLE [dbo].[Currencies] WITH NOCHECK
ADD CONSTRAINT [FK_Currencies_MainServerData]
FOREIGN KEY([OriginServer]) REFERENCES [dbo].[MainServerData] ([ServerId])
ON UPDATE CASCADE
This is the code that is generated (I've omitted a lot of unrelated fields):
public partial class MainServerData
{
public MainServerData()
{
Currencies = new HashSet<Currency>();
}
[Key]
[Column(TypeName = "numeric(20, 0)")]
public decimal ServerId { get; set; }
[InverseProperty(nameof(Currency.OriginServerNavigation))]
public virtual ICollection<Currency> Currencies { get; set; }
}
[Index(nameof(OriginServer), Name = "IX_OriginServer_Currencies")]
[Index(nameof(SpecialKey), Name = "IX_SpecialKey_Currencies")]
public partial class Currency
{
public Currency()
{
Wallets = new HashSet<Wallet>();
}
[Key]
public int Id { get; set; }
[Column(TypeName = "numeric(20, 0)")]
public decimal OriginServer { get; set; }
[Required]
[StringLength(64)]
public string SpecialKey { get; set; }
[Required]
[StringLength(16)]
public string Symbol { get; set; }
[ForeignKey(nameof(OriginServer))]
[InverseProperty(nameof(MainServerData.Currencies))]
public virtual MainServerData OriginServerNavigation { get; set; }
[InverseProperty(nameof(Wallet.Currency))]
public virtual ICollection<Wallet> Wallets { get; set; }
}
And part of the DbContext code:
modelBuilder.Entity<Currency>(entity =>
{
entity.HasOne(d => d.OriginServerNavigation)
.WithMany(p => p.Currencies)
.HasForeignKey(d => d.OriginServer)
.OnDelete(DeleteBehavior.ClientSetNull)
.HasConstraintName("FK_Currencies_MainServerData");
});
Until I had to use the FK, it was all working good. But now when I try to access:
database.MainServerData
.FirstOrDefault(s => s.ServerId == decimalId)?.Currencies?.FirstOrDefault()
This is always null, even when there are entries in the database.
I can't figure out what the problem is.
EDIT:
To continue work, while investigating the issue, I decided to create a helper method, that returns the Currency as intended:
public partial class MainServerData
{
public Currency GetCurrency(DatabaseContextBase database)
{
return database.Currencies.FirstOrDefault(c => c.OriginServer == this.ServerId);
}
}
In addition, Currencies also have Wallets FK, which also does not work:
modelBuilder.Entity<Wallet>(entity =>
{
entity.HasOne(d => d.Currency)
.WithMany(p => p.Wallets)
.HasForeignKey(d => d.CurrencyId)
.OnDelete(DeleteBehavior.ClientSetNull)
.HasConstraintName("FK_Wallets_Currencies");
});
This is in the Currency class:
[InverseProperty(nameof(Wallet.Currency))]
public virtual ICollection<Wallet> Wallets { get; set; }
Yet this returns 0 (pickedCurrency is correct and it has wallets in the DB)
var walletCount = pickedCurrency.Wallets.Count();
I assume this could be an issue with EntityFramework Core, but I have not managed to confirm this suspicion. Perhaps my version is outdated, it's v5.0.7? I will try to update it. (edit: updated to v5.0.17, did not fix the problem)

EF core navigation property return null value even after using Incude

This is not a duplicate question as I have looked up many questions including this, which is the closest to what I want but didn't solve the challenge.
I have my table models relation set up this way:
public class User
{
public long UserId { get; set; }
public string Name { get; set; }
public IList<Transaction> Transactions { get; set; }
}
public class Transaction
{
public long TransactionId { get; set; }
public User User { get; set; }
public User Patient { get; set; }
}
fluent api setup for the entity
//some other modelbuilder stuff
modelBuilder.Entity<User>(entity =>
{
entity.HasMany(e => e.Transactions).WithOne(e => e.User);
//wanted to add another entity.HasMany(e => e.User).WithOne(e => e.Patient) but efcore didn't allow me.
});
This generates a Transaction table with UserUserId and PatientUserId and takes the right values on save.
But when I do a get with a user Id
User user = dbcontext.Set<User>().Include(t => t.Transactions).FirstOrDefault(u => u.UserId == userId);
user.Transactions have a list of transaction all with null Transaction.Patient
What exactly is going on here and how do I get past it?
Thanks.
You are nesting navigations. So, you have to use ThenInclude like this to add Patient which is a navigation property of Transaction.
User user = dbcontext.Set<User>().Include(t => t.Transactions).ThenInclude(p => p.Patient).FirstOrDefault(u => u.UserId == userId);

Map table row to DDD value object woith EF core

scenario
I have two aggregates in my microservice Domain model: User and UserNotification.
public class User : Aggreagete
{
public int Id { get; private set; }
}
public class UserNotification : Aggreagete
{
public int Id { get; private set; }
public int UserId { get; private set; }
public bool EnableSms { get; private set; }
public bool EnableEmail { get; private set; }
public NotificationKind NotificationKind { get; private set; }
public UserModel User { get;private set }
}
UserValueObject needs to be mapped from the same table as User Aggregate. I use UserValueObject because from Notification bounded Context it needs to be immutable.
I have following EF mappings:
public void Configure(EntityTypeBuilder<User> builder)
{
builder.ToTable("Users", SCHEME);
builder.HasKey(o => o.Id);
builder.Property(o => o.Id);
builder.HasMany<UserNotification>()
.WithOne()
.HasForeignKey(entry => entry.UserId)
.OnDelete(DeleteBehavior.Cascade);
}
public void Configure(EntityTypeBuilder<UserNotification> builder)
{
builder.ToTable("UserNotifications", SCHEME);
builder.HasKey(o => o.Id);
builder.Property(o => o.Id);
builder.Property(e => e.UserId).IsRequired();
builder.Property(e => e.EnableSms).IsRequired();
builder.Property(e => e.EnableEmail).IsRequired();
builder.Property(e => e.NotificationKind).IsRequired()
.HasConversion(
v => v.ToString(),
strValue => Enum.Parse<NotificationKind>(strValue)
);
builder.OwnsOne(e => e.User).ToTable("Users");
}
problem
I receive error during "Add Migration"
Cannot use table 'dbo.Users' for entity type 'User' since it is being used for entity type 'UserModel' and there is no relationship between their primary keys.
UserModel is:
public class UserModel : ValueObject
{
public int Id { get; private set; }
public string Email { get; private set; }
public string Phone { get; private set; }
public string Name { get; private set; }
}
question
I would like to map UserModel.Id from database table "Users", but Email, Phone and Name should be injected in repository with using HTTP request to Identity
Service. Is is possible to do such operation with EF core?
I see that it is not possible to achieve this with owned entity, because Users table is used also to mapped other aggreagate.
You are getting the correct exception due to the name conflict.
For the User entity you ask EF to use the Users table:
builder.ToTable("Users", SCHEME);
but you also ask it to use the same table to store value objects of the UserNotification entity:
builder.OwnsOne(e => e.User).ToTable("Users");
If your plan is to "reuse" the Users aggregate persistence to magically map the value project to it than I can definitely tell that it's not a good idea.
Value objects are typed pieces of state. They belong to entities and managed by entities. You have to update the content of the VO when needed.
You also don't need to keep the user information in the aggregate, unless you use it in any of the methods of your notification aggregate. If you need to be able to query and join - just do that in the query. Don't use EF for queries and write a plain SQL with inner join to get the information you need. Look up CQRS, Jimmy Bogard advocated using EF for the write side and plain SQL queries for the read side some time ago, it makes total sense.

How to fix: The number of properties in the Dependent and Principal Roles in a relationship constraint must be identical?

I am using Entity Framework 4.3.1 against a SQL Server 2012 database and I am using the POCO approach. I am getting the following error and I am wondering if anyone can explain how to fix it:
ModelValidationException
One or more validation errors were detected during model generation:
\tSystem.Data.Entity.Edm.EdmAssociationConstraint: : The number of properties in the Dependent and Principal Roles in a relationship constraint must be identical.
There is no InnerException available for any further information.
I cannot change the database schema and it is a little odd, but here it is...
** are the primary key (notice I have composite primary keys)
(FK) Denotes a foreign key
Here are the tables (if it helps I can post the SQL to generate them but I do not think the tables are actually the problem as the exception is in the validation of the model):
One
-
**OneId int not null
**TwoId int not null (FK)
**ThreeId int not null (FK)
Name nvarchar(50) not null
Two
-
**TwoId int not null
**ThreeId int not null (FK)
Name nvarchar(50) not null
Three
-
**ThreeId not null
Name nvarchar(50) not null
Here are the entities (notice that I am including the foreign keys in the model but other than that pretty standard):
public class Three
{
public int ThreeId { get; set; }
public string Name { get; set; }
public virtual ICollection<Two> Twos { get; private set; }
public virtual ICollection<One> Ones { get; private set; }
public void AddOne(One one)
{
if (one == null)
throw new ArgumentNullException("two");
if (Ones == null)
Ones = new List<One>();
if (!Ones.Contains(one))
Ones.Add(one);
one.Three = this;
}
public void AddTwo(Two two)
{
if (two == null)
throw new ArgumentNullException("two");
if (Twos == null)
Twos = new List<Two>();
if (!Twos.Contains(two))
Twos.Add(two);
two.Three = this;
}
}
public class Two
{
public int TwoId { get; set; }
public int ThreeId { get; set; }
public string Name { get; set; }
public virtual Three Three { get; set; }
public virtual ICollection<One> Ones { get; private set; }
public void AddOne(One one)
{
if (one == null)
throw new ArgumentNullException("two");
if (Ones == null)
Ones = new List<One>();
if (!Ones.Contains(one))
Ones.Add(one);
one.Two = this;
}
}
public class One
{
public int OneId { get; set; }
public int TwoId { get; set; }
public int ThreeId { get; set; }
public virtual Two Two { get; set; }
public virtual Three Three { get; set; }
}
And here is the data context:
public class DbCtx : DbContext
{
public DbCtx(string connectionString)
: base(connectionString)
{
Ones = Set<One>();
Twos = Set<Two>();
Threes = Set<Three>();
}
public DbSet<One> Ones { get; private set; }
public DbSet<Two> Twos { get; private set; }
public DbSet<Three> Threes { get; private set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
var one = modelBuilder.Entity<One>();
one.ToTable("One");
one.HasKey(d => new
{
d.OneId,
d.TwoId,
d.ThreeId
});
one.Property(d => d.OneId)
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.None);
one.HasRequired(t => t.Two)
.WithMany(s => s.Ones)
.HasForeignKey(t => t.TwoId);
one.HasRequired(t => t.Three)
.WithMany(s => s.Ones)
.HasForeignKey(t => t.ThreeId);
var two = modelBuilder.Entity<Two>();
two.ToTable("Two");
two.HasKey(d => new
{
d.TwoId,
d.ThreeId
});
two.Property(p => p.TwoId)
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.None);
two.HasRequired(t => t.Three)
.WithMany(s => s.Twos)
.HasForeignKey(t => t.ThreeId);
var three = modelBuilder.Entity<Three>();
three.ToTable("Three");
three.HasKey(s => s.ThreeId);
three.Property(p => p.ThreeId)
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.None);
base.OnModelCreating(modelBuilder);
}
}
Finally, this is a snippet of code to cause the exception:
using (var ctx = new DbCtx(#"....."))
{
Console.WriteLine(ctx.Twos.Count());
}
The reason for the error are incorrectly configured relations in your model. This is not correct:
one.HasRequired(t => t.Two)
.WithMany(s => s.Ones)
.HasForeignKey(t => t.TwoId);
one.HasRequired(t => t.Three)
.WithMany(s => s.Ones)
.HasForeignKey(t => t.ThreeId);
It should be:
one.HasRequired(t => t.Two)
.WithMany(s => s.Ones)
.HasForeignKey(t => new { t.TwoId, t.ThreeId });
Because dependent's FK must contain all columns of principal PK. You must also remove navigation property from Three to One.
This can also be caused by Code first from Database.
I had several views that I brought in that did not have an obvious key field according to Entity Framework conventions. The code generated put the [Key] attribute on the wrong field. In fact, it could not detect any uniqueness, so it put the [Key] attribute on all the fields.
I was able to remove all of the extra Key attributes to make the error go away.
Note for EF5+:
.HasForeignKey has been deprecated from EF 5: List of available methods (https://msdn.microsoft.com/en-us/library/system.data.entity.modelconfiguration.configuration.manytomanyassociationmappingconfiguration_methods(v=vs.103).aspx)
- MapLeftKey
- MapRightKey
- ToTable
If one were to need Many to Many where one 'Many' is to an Entity with a CompositeKey is:
one.HasKey(t => new { t.TwoId, t.ThreeId });
one.HasRequired(t => t.Two)
.WithMany(s => s.Ones)
.Map(m=>m.MapLeftKey("OneId").MapRIghtKey(new string[]{"TwoId", "ThreeId"}))

Categories