EF - 2:Many relationship not working as expected - c#

I have Payments between Users. A Payment has a FromUser and a ToUser. I'm testing out the entity relationships with the following:
var newPayment = new Payment() {
FromUserId = user1.UserId,
ToUserId = user2.UserId
};
db.Payments.Add(newPayment);
db.SaveChanges();
var tempPaymentId = user1.Payments.First().PaymentId;
newPayment = db.Payments.First(s => s.PaymentId == tempPaymentId);
Assert.AreEqual(newPayment.FromUserId, user1.UserId); // true
Assert.AreEqual(newPayment.ToUserId, user2.UserId); // true
Assert.AreEqual(user1.Payments.Count(), 1); // true
Assert.AreEqual(user2.Payments.Count(), 1); // false
My question is - why does user2 not have any Payments?
Class and fluent config:
public class Payment {
[Key]
[DatabaseGeneratedAttribute(DatabaseGeneratedOption.Identity)]
public int PaymentId { get; set; }
[ForeignKey("FromUser")]
public int FromUserId { get; set; }
public User FromUser { get; set; }
[ForeignKey("ToUser")]
public int ToUserId { get; set; }
public User ToUser { get; set; }
}
public PaymentConfiguration() {
HasRequired(s => s.FromUser);
HasRequired(s => s.ToUser);
}
public UserConfiguration() {
// One-to-Many
HasMany(s => s.Payments);
}

It can be due to many reasons, but, first things first, you mixed Data Annotations and Fluent API mappings. Why? I am not sure if it works as expected or not, but I am sure that it does not look right. Stick to one (I recommend Fluent API). Secondly, I suppose that, you should have two navigation properties at User for Payments. User.Payments does not mean much, do you want payments that the user paid, or the payments that were paid to user by one navigation property?. And you have to map only from one side. Finally, your models and mappings should be like below:
public class User
{
public int UserId { get; set; }
// Navigation properties
public virtual ICollection<Payment> PaymentsFromUser { get; set; }
public virtual ICollection<Payment> PaymentsToUser { get; set; }
}
public class UserConfiguration
: IEntityTypeConfiguration<User>
{
public UserConfiguration()
{
// Primary key
HasKey(m => m.UserId);
Property(m => m.PaymentId)
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
}
}
public class Payment
{
public int PaymentId { get; set; }
public int FromUserId { get; set; }
public int ToUserId { get; set; }
// Navigation properties
public virtual User FromUser { get; set; }
public virtual User ToUser { get; set; }
}
public class PaymentConfiguration
: IEntityTypeConfiguration<Payment>
{
public PaymentConfiguration()
{
// Primary key
HasKey(m => m.PaymentId);
Property(m => m.PaymentId)
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
// Relationship mappings
HasRequired(m => m.FromUser)
.WithMany(m => m.PaymentsFromUser)
.HasForeignKey(m => m.FromUserId)
.WillCascadeOnDelete(true);
HasRequired(m => m.ToUser)
.WithMany(m => m.PaymentsToUser)
.HasForeignKey(m => m.ToUserId)
.WillCascadeOnDelete(true);
}
}
Note: virtual keyword is for Lazy loading and can be skipped if you do not need it.
Now it should work as expected. Just remember to test like:
Assert.AreEqual(user1.PaymentsFromUser.Count(), 1);
Assert.AreEqual(user2.PaymentsToUser.Count(), 1);
Please note that, I suppose user1 and user2 both are tracked by EntityFramework, so EF can relate users with payments.

Related

Efcore issue with many 2 many on same entity (Products & RelatedProducts)

So i want to add RelatedProducts to my products. So i applied the same relationship type as i did for category. The only difference there is no category class, but we target the same entity. (product). Because the regular many2many works fine, i removed that from my example.
Goal:
Any suggestions?
Exception:
"Exception occured: Cannot create a relationship between 'Product.RelatedProducts' and 'RelatedProduct.Related' because a relationship already exists between 'Product.RelatedProducts' and 'RelatedProduct.Product'. Navigation properties can only participate in a single relationship. If you want to override an existing relationship call 'Ignore' on the navigation 'RelatedProduct.Related' first in 'OnModelCreating'."
Product.cs:
public class Product : IExportable, IEntityBase
{
public int Id { get; set; }
public string Name { get; set;}
public ICollection<RelatedProduct> RelatedProducts { get; set; }
}
ProductEntityTypeConfiguration.cs
internal class ProductEntityTypeConfiguration : IEntityTypeConfiguration<Product>
{
public void Configure(EntityTypeBuilder<Product> builder)
{
config.HasKey(p => p.Id);
}
}
RelatedProducts.cs
public class RelatedProduct
{
public int Id { get; set; }
public int ProductId { get; set; }
public virtual Product Product { get; set; }
public int RelatedId { get; set; }
public virtual Product Related { get; set; }
}
RelatedProductEntityTypeConfiguration.cs
public class RelatedProductsEntityConfiguration : IEntityTypeConfiguration<RelatedProduct>
{
public void Configure(EntityTypeBuilder<RelatedProduct> builder)
{
builder.HasKey(rp => rp.Id);
builder.ToTable("RelatedProducts");
builder
.HasOne(rp => rp.Product)
.WithMany(p => p.RelatedProducts)
.HasForeignKey(rp => rp.ProductId)
.OnDelete(DeleteBehavior.Restrict);
builder
.HasOne(rp => rp.Related)
.WithMany(p => p.RelatedProducts)
.HasForeignKey(rp => rp.RelatedId)
.OnDelete(DeleteBehavior.Restrict);
}
}
I am unable to test this suggestion but I am sure it might work. Why don't you just define your RelatedProduct as:
public class RelatedProduct
{
public int Id { get; set; }
public virtual ICollection<Product> Related { get; set; }
}
I also suggest you comment out the code in your RelatedProductsEntityConfiguration Configure method

Entity Framework Core: Many To Many relationship with some null values for some records

Let's say I have the following 3 classes - a Company that employs ConstructionWorkers and TruckDrivers. Let's say those people can be employed at many companies (as they work part time) - so it's a many to many relationship.
Some companies will only employ TruckDrivers, others will only employ ConstructionWorkers and others yet will employ both. This means in my CompanyEmployeeMapping table, the record could look like this:
CompanyEmployeeMapping Table:
+-------------+------------------+------------------------+
| CompanyId | TruckDriverId | ConstructionWorkerId |
+-------------+------------------+------------------------+
| 1 | 10 | NULL |
+-------------+------------------+------------------------+
(ie - Company only employs Truck Drivers)
This is how I set up my Class Structure + Fluent API:
public class Company
{
public int Id { get; set; }
public string Name { get; set; }
public ICollection<CompanyEmployeeMapping> Employees { get; set; }
}
public class ConstructionWorker
{
public int Id { get; set; }
public string Name { get; set; }
public ICollection<CompanyEmployeeMapping> Companies { get; set; }
}
public class TruckDriver
{
public int Id { get; set; }
public string Name { get; set; }
public ICollection<CompanyEmployeeMapping> Companies { get; set; }
}
public class CompanyEmployeeMapping
{
public int CompanyId { get; set; }
public int TruckDriverId { get; set; }
public int ConstructionWorkerId { get; set; }
[JsonIgnore]
public Company LinkedCompany { get; set; }
[JsonIgnore]
public TruckDriver LinkedTruckDriver { get; set; }
[JsonIgnore]
public ConstructionWorker LinkedConstructionWorker { get; set; }
}
Fluent API setup:
builder.Entity<CompanyEmployeeMapping>()
.HasKey(x => new { x.CompanyId, x.TruckDriverId, x.ConstructionWorkerId });
builder.Entity<CompanyEmployeeMapping>()
.HasOne(c => c.LinkedCompany)
.WithMany(m => m.LinkedEmployees)
.HasForeignKey(x => x.CompanyId)
.HasPrincipalKey(x => x.Id);
builder.Entity<CompanyEmployeeMapping>()
.HasOne(c => c.LinkedTruckDriver)
.WithMany(m => m.LinkedCompanies)
.HasForeignKey(x => x.TruckDriverId)
.HasPrincipalKey(x => x.Id);
builder.Entity<CompanyEmployeeMapping>()
.HasOne(c => c.LinkedConstructionWorker)
.WithMany(m => m.LinkedCompanies)
.HasForeignKey(x => x.ConstructionWorkerId)
.HasPrincipalKey(x => x.Id);
This seems to work correctly for me - I get an object with companies, containing a list of TruckDrivers and a list of ConstructionWorkers. Each of those also have their corresponding companies.
QUESTION
When I try to add a new company, which only employs TruckDrivers I get an exception:
The value of CompanyEmployeeMapping.ConstructionWorkerId is unknown when attempting to save changes. This is because the property is also part of a foreign key for which the principal entity in the relationship is not known.
What am I doing wrong here?
It looks like each instance of the mapping entity will only map to either TruckDriver or ConstructionWorker.
Therefore, your TruckDriver and ConstructionWorker ids need to be nullable.
This will then create a new challenge that your composite key cannot contain a nullable property.
I think the way forward is to make those ints nullable and then create a new single property EmployeeMapping Id for the mapping entity key.

EF Core - many to many relationship use / access custom join table

I am trying to implement a many to many relationship.
The Models -
public class User
{
[Key]
public int Id { get; set; }
public string Name { get; set; }
public List<Book> OwnedBooks { get; set; }
}
public class Own
{
public int UserId { get; set; }
public int BookId { get; set; }
public User User { get; set; }
public Book Book { get; set; }
}
public class Book
{
[Key]
public int Id { get; set; }
public int AuthorId { get; set; }
public User Author { get; set; }
public List<User> OwnedBy { get; set; } //Not really needed, but without it I can't create the join table "Own"
[NotMapped]
public int UsersReached; //Get this via the "Own" table
}
The DbContext -
public class TestContext : DbContext
{
public DbSet<User> Users { get; set; }
public DbSet<Book> Books { get; set; }
public DbSet<Own> Own { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder options) => options.UseSqlServer("Server=DESKTOP-BT4H8CA;Database=Test;Trusted_Connection=True");
protected override void OnModelCreating(ModelBuilder builder)
{
builder.Entity<Book>().HasOne(x => x.Author);
builder.Entity<User>()
.HasMany(x => x.OwnedBooks)
.WithMany(x => x.OwnedBy)
.UsingEntity(x => x.ToTable("Own"));
builder.Entity<Own>()
.HasKey(x => new {x.BookId, x.UserId});
}
}
I am struggling with accessing the join table "Own". I need it to get the amount of each Book that is sold, without completely loading the users. That's why I don't want to use the auto generated one:
Cannot use table 'Own' for entity type 'BookUser (Dictionary<string, object>)' since it is being used for entity type 'Own' and potentially other entity types, but there is no linking relationship. Add a foreign key to 'BookUser (Dictionary<string, object>)' on the primary key properties and pointing to the primary key on another entity typed mapped to 'Own'.
Thanks in advance for your help!
You can actually use the auto-generated joining table and still get the count of each book sold, without completely loading the users.
With your current User and Book models, configure the relationships as -
protected override void OnModelCreating(ModelBuilder builder)
{
builder.Entity<Book>()
.HasOne(p => p.Author)
.WithMany()
.HasForeignKey(p => p.AuthorId)
.OnDelete(DeleteBehavior.NoAction);
builder.Entity<User>()
.HasMany(p => p.OwnedBooks)
.WithMany(p => p.OwnedBy);
}
Then you can query the books with their count of sales as -
var books = dbCtx.Books
.Select(p => new Book
{
Id = p.Id,
AuthorId = p.AuthorId,
Author = p.Author,
UsersReached = p.OwnedBy.Count // this will not load User entities
})
.ToList();
EDIT:
You can use AutoMapper which can do the projection in .Select() method for you, like -
var dtos = _Mapper.ProjectTo<BookDTO>(dbCtx.Books).ToList();
For that, you'll need to -
create a DTO model with properties you want from the query result, like -
public class BookDTO
{
public int Id { get; set; }
public string Author { get; set; }
public int UsersReached { get; set; }
}
define a map from Book to BookDTO -
CreateMap<Book, BookDTO>()
.ForMember(d => d.Author, opt => opt.MapFrom(s => s.Author.Name))
.ForMember(d => d.UsersReached, opt => opt.MapFrom(s => s.OwnedBy.Count));
You can remove the [NotMapped] property UsersReached from the Book model.

EF Core: Invalid column name 'companyId1'

I have trouble understanding why a EF generated SELECT clause contains the primary key twice, the second one is postfixed with '1'.
exec sp_executesql N'SELECT [entity.WebAdminCompanyUser].[CompanyId], [entity.WebAdminCompanyUser].[AspNetUserId], [entity.WebAdminCompanyUser].[CompanyId1]
FROM [SafeProtect].[WebAdminCompanyUser] AS [entity.WebAdminCompanyUser]
INNER JOIN (
SELECT [entity1].[AspNetUserId]
FROM [SafeProtect].[WebAdminUser] AS [entity1]
WHERE ([entity1].[RowEnabled] = 1) AND EXISTS (
SELECT 1
FROM [SafeProtect].[WebAdminCompanyUser] AS [companyUser1]
WHERE ([companyUser1].[CompanyId] = #__companyId_0) AND ([entity1].[AspNetUserId] = [companyUser1].[AspNetUserId]))
) AS [t0] ON [entity.WebAdminCompanyUser].[AspNetUserId] = [t0].[AspNetUserId]
ORDER BY [t0].[AspNetUserId]',N'#__companyId_0 int',#__companyId_0=1
It fails with Invalid column name 'CompanyId1'.
Following are the entities and the corresponding configurations (fluent API):
WebAdminCompanyUser:
public partial class WebAdminCompanyUser : ITrackable, IMergeable
{
public WebAdminCompanyUser()
{
AdditionalInit();
}
public int CompanyId { get; set; }
public int AspNetUserId { get; set; }
public virtual Company Company { get; set; }
[NotMapped]
public TrackingState TrackingState { get; set; }
[NotMapped]
public ICollection<string> ModifiedProperties { get; set; }
[NotMapped]
public Guid EntityIdentifier { get; set; }
partial void AdditionalInit();
}
}
Configuration:
builder.Entity<WebAdminCompanyUser>(entity =>
{
entity.ToTable(name: "WebAdminCompanyUser", schema: SqlSchema.SafeProtect);
entity.HasKey("CompanyId", "AspNetUserId");
entity
.HasOne(d => d.Company)
.WithMany()
.HasForeignKey(d => d.CompanyId)
.IsRequired();
});
WebAdminUser:
public partial class WebAdminUser : IdentityUser<int>, IAuditInfo, IRowDisableableWithDateTime, ITrackable, IMergeable
{
public WebAdminUser()
{
WebAdminCompanyUser = new HashSet<WebAdminCompanyUser>();
WebAdminUserRole = new HashSet<WebAdminUserRole>();
WebAdminUserClaim = new HashSet<WebAdminUserClaim>();
WebAdminUserLogin = new HashSet<WebAdminUserLogin>();
AdditionalInit();
}
public string DisplayName { get; set; }
public DateTime CreatedOn { get; set; }
public DateTime? ModifiedOn { get; set; }
public bool RowEnabled { get; set; }
public DateTime? DisabledOn { get; set; }
public virtual ICollection<WebAdminCompanyUser> WebAdminCompanyUser { get; set; }
public virtual ICollection<WebAdminUserRole> WebAdminUserRole { get; set; }
public virtual ICollection<WebAdminUserClaim> WebAdminUserClaim { get; set; }
public virtual ICollection<WebAdminUserLogin> WebAdminUserLogin { get; set; }
[NotMapped]
public TrackingState TrackingState { get; set; }
[NotMapped]
public ICollection<string> ModifiedProperties { get; set; }
[NotMapped]
public Guid EntityIdentifier { get; set; }
partial void AdditionalInit();
}
Configuration:
builder.Entity<WebAdminUser>(entity =>
{
entity.ToTable(name: "WebAdminUser", schema: SqlSchema.SafeProtect);
entity.Property(e => e.Id).HasColumnName("AspNetUserId");
// authorize multiple user name
entity.HasIndex((p) => new { p.UserName }).IsUnique(false);
entity
.HasMany(user => user.WebAdminUserClaim)
.WithOne()
.HasForeignKey(userClaims => userClaims.UserId)
.IsRequired();
entity
.HasMany(user => user.WebAdminUserLogin)
.WithOne()
.HasForeignKey(userLogin => userLogin.UserId)
.IsRequired();
entity
.HasMany(user => user.WebAdminUserRole)
.WithOne()
.HasForeignKey(userRole => userRole.UserId)
.IsRequired();
entity
.HasMany(user => user.WebAdminCompanyUser)
.WithOne()
.HasForeignKey(companyUser => companyUser.AspNetUserId)
.IsRequired();
});
EF query:
IQueryable<WebAdminUser> query =
from WebAdminUser user WebAdminUserRepository.All()
.Include(user => user.WebAdminUserRole)
.ThenInclude(userRole => userRole.AspNetRole)
.Include(user => user.WebAdminCompanyUser)
where user.WebAdminCompanyUser.Any(companyUser => companyUser.CompanyId == companyId)
select user;
return query.ToList();
Any help appreciated.
This usually happens when you have improperly mapped relationship by leaving some navigation property out of fluent configuration.
Remember that each navigation property (collection or reference) represents a relationship. If you fluently configure relationships and use HasOne / HasMany / WithOne / WithMany w/o passing the navigation property, you are telling EF that the relationship has no navigation property for the corresponding end. But if you actually do have navigation property, EF will map it to a separate relationship with default FK column name. If the default property/column name is already used, EF will append index to it until it gets unique.
In your case, the WebAdminUser class and configuration you've shown are irrelevant. The invalid column name CompanyId1 indicates that the problem is with Company class which you haven't shown, and the WithMany() call here
.HasOne(d => d.Company)
.WithMany() // <--
Most likely your Company class has collection navigation property to WebAdminCompanyUser, something like this (virtual and the name of the property doesn't matter):
public virtual ICollection<WebAdminCompanyUser> CompanyUsers { get; set; }
then you need to change the above .WithMany() call with something like
.WithMany(c => c.CompanyUsers)
and the problem will be solved.

Entity Framework One to Many Relations ship Fluent API with composite keys

Below are the classes that I am using. I want to create a one way reference from Stratification to PatientClientPhysician. I can also do a one to many relationship if that is required by Entity Framework but this will always be a 1-1 even though this is a linker table.
I have to use a composite key for this ClientId, PatientId, PhysicianId. the errors that I keep getting are.
One or more validation errors were detected during model generation:
SPM.Data.Stratification_Physician: : Multiplicity conflicts with the referential constraint in Role 'Stratification_Physician_Target' in relationship 'Stratification_Physician'. Because all of the properties in the Dependent Role are non-nullable, multiplicity of the Principal Role must be '1'.
Stratification_PrimaryPhysician_Target_Stratification_PrimaryPhysician_Source: : The number of properties in the Dependent and Principal Roles in a relationship constraint must be identical.
PatientClientPhysician_Stratifications_Target_PatientClientPhysician_Stratifications_Source: : The number of properties in the Dependent and Principal Roles in a relationship constraint must be identical.
What is the correct fluent api calls for this?
I would really like to go through the PatientClientPhysician class straight to Physician if that is possible.
Thanks
public class PatientClientPhysician : BaseEntity
{
public Guid ClientId { get; set; }
public Guid PatientId { get; set; }
public Guid PhysicianId { get; set; }
public virtual Client Client { get; set; }
public virtual Patient Patient { get; set; }
public virtual Physician Physician { get; set; }
public PatientClientPhysician()
: base()
{
}
}
public class PatientClientPhysicianConfiguration :
EntityConfigurationBase<PatientClientPhysician>
{
public PatientClientPhysicianConfiguration()
{
ToTable("patientclientphysician");
//Property(m => m.Id).HasColumnName("id");
Property(m => m.ClientId).HasColumnName("clientid");
Property(m => m.PatientId).HasColumnName("patientid");
Property(m => m.PhysicianId).HasColumnName("physicianid");
HasRequired(m => m.Patient).WithMany().HasForeignKey(p => p.PatientId);
HasRequired(m => m.Client).WithMany().HasForeignKey(c => c.ClientId);
HasRequired(m => m.Physician).WithMany().HasForeignKey(p => p.PhysicianId);
}
}
public class Stratification : BaseEntity
{
public Stratification()
{
LabOrders = new List<StratLabOrder>();
Observations = new List<StratObs>();
Contacts = new List<StratContact>();
StratIcd9s = new List<StratIcd9>();
}
public Guid ClientLocationId { get; set; }
public Guid PatientId { get; set; }
public Guid StratInputId { get; set; }
public Guid ClientId { get; set; }
public Guid PhysicianId { get; set; }
public virtual ClientLocation ClientLocation { get; set; }
public virtual Patient Patient { get; set; }
public virtual StratInput StratInput { get; set; }
public string EpisodeNumber { get; set; }
public virtual Physician Physician { get; set; }
public PrintStatus LetterPrintStatus { get; set; }
public virtual ICollection<StratLabOrder> LabOrders { get; set; }
public virtual ICollection<StratObs> Observations { get; set; }
public virtual ICollection<StratContact> Contacts { get; set; }
public virtual ICollection<StratIcd9> StratIcd9s { get; set; }
public virtual PatientClientPhysician PrimaryPhysician { get; set; }
}
//this is in EntityConfigurationBase<Stratification> class
HasRequired(m => m.PrimaryPhysician).WithRequired().HasForeignKey(p => new { p.ClientId, p.PatientId, p.PhysicianId });
edit 1
I added
HasKey(t => new { t.ClientId, t.PatientId, t.PhysicianId });
To the PatientClientPhysicianConfiguration class and get this error.
One or more validation errors were detected during model generation:
SPM.Data.Stratification_Physician: : Multiplicity conflicts with the referential constraint in Role 'Stratification_Physician_Target' in relationship 'Stratification_Physician'. Because all of the properties in the Dependent Role are non-nullable, multiplicity of the Principal Role must be '1'.
This is what I have the relationship set to right now.
HasRequired(m => m.PatientClientPhysicians).WithMany().HasForeignKey(p => new { p.ClientId, p.PatientId, p.PhysicianId });
Try this:
public class PatientClientPhysicianConfiguration :
EntityConfigurationBase<PatientClientPhysician>
{
public PatientClientPhysicianConfiguration()
{
ToTable("patientclientphysician");
// Composite key:
HasKey(t => new { t.ClientId, t.PatientId, t.PhysicianId });
Property(m => m.ClientId).HasColumnName("clientid");
Property(m => m.PatientId).HasColumnName("patientid");
Property(m => m.PhysicianId).HasColumnName("physicianid");
HasRequired(m => m.Patient).WithMany().HasForeignKey(p => p.PatientId);
HasRequired(m => m.Client).WithMany().HasForeignKey(c => c.ClientId);
HasRequired(m => m.Physician).WithMany().HasForeignKey(p => p.PhysicianId);
}
}

Categories