EF Core Relationship mapping - c#

In project I can have one User that can have many UserActivites. In my models I've set up their relationship as follows:
public class User
{
public int Id { get; set; }
public string UserName { get; set; }
public string FirstName { get; set; }
public string Surname { get; set; }
public string Email { get; set; }
//relationship mapping example
// delete these attributes and you'll cause a self referenceing loop error
[JsonIgnore]
[IgnoreDataMember]
public List<UserActivity> Activities { get; set; }
}
public class UserActivity
{
public int Id { get; set; }
public string UserName { get; set; }
public string Email { get; set; }
public string Project { get; set; }
public DateTime EntryDate { get; set; }
// relationship mapping
public User User { get; set; }
}
And in my repository class, I'm getting all my user activities this way:
public async Task<IEnumerable<UserActivity>> GetAll()
{
var result = await _context.UserActivities.Include(activity => activity.User).OrderByDescending(x => x.EntryDate).ToListAsync();
return result;
}
However, when I run my project, the User property of UserActivities is null. So I checked the Microsoft docs on EF Core relationships and updated my OnModelCreating method inside of my context to also do the mapping as follows:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<UserActivity>().ToTable("UserActivities").Property(x => x.Id).ValueGeneratedOnAdd();
modelBuilder.Entity<UserActivity>().ToTable("UserActivities").HasOne(x => x.User).WithMany();
modelBuilder.Entity<User>().ToTable("Users").Property(x => x.Id).ValueGeneratedOnAdd();
base.OnModelCreating(modelBuilder);
}
However, when I run the project again, my User property still isn't populated. I know this isn't a data issue as I have data inside of my User table and display that on a separate page.
I'm not sure what I'm doing wrong/missing with this. So any help would be appreciated

Try this code:
Your tables:
public partial class User
{
public User()
{
UserActivities = new HashSet<UserActivity>();
}
[Key]
public int Id { get; set; }
[Required]
public string UserName { get; set; }
[Required]
public string FirstName { get; set; }
[Required]
public string LastName { get; set; }
public string Email { get; set; }
[InverseProperty(nameof(UserActivity.User))]
public virtual ICollection<UserActivity> UserActivities { get; set; }
}
public partial class UserActivity
{
[Key]
public int Id { get; set; }
public int UserId { get; set; }
public string Project { get; set; }
public DateTime EntryDate { get; set; }
[ForeignKey(nameof(UserId))]
[InverseProperty("UserActivity")]
public virtual User User { get; set; }
}
dbcontext:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<UserActivity>(entity =>
{
entity.HasOne(d => d.User)
.WithMany(p => p.UserActivities)
.HasForeignKey(d => d.UserId)
.OnDelete(DeleteBehavior.ClientSetNull)
.HasConstraintName("FK_UserActivity_User");
});
OnModelCreatingPartial(modelBuilder);
}

Related

Dotnet Api Auto Mapper on Many to Many Relation won't work

I have three classes for many to relation in dotnet core Api. Here's how they look like ;
public class Job
{
[Key]
public int Id { get; set; }
[Required]
[MaxLength(70)]
public string Caption { get; set; }
[Required]
[MaxLength(500)]
public string Detail { get; set; }
public int Urgency { get; set; }
public bool IsFinished { get; set; } = false;
public List<User_Has_Job> user_Has_jobs { get; set; }
}
public class User
{
[Key]
public int Id { get; set; }
[Required]
[MaxLength(250)]
public string Name { get; set; }
[Required]
[MaxLength(250)]
public string LastName { get; set; }
[Required]
[MaxLength(250)]
public string Mail { get; set; }
[Required]
[MaxLength(250)]
public string Passwd { get; set; }
public List<User_Has_Job> user_Has_jobs { get; set; }
}
public class User_Has_Job
{
public int UserId { get; set; }
public User user { get; set; }
public int JobId { get; set; }
public Job job { get; set; }
}
And here is how my context class looks like ;
public class TodoApiContext : DbContext
{
public TodoApiContext(DbContextOptions<TodoApiContext> opt) : base(opt)
{
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
ManyToManyRelationshipConfiguration(modelBuilder);
}
private void ManyToManyRelationshipConfiguration(ModelBuilder modelBuilder)
{
modelBuilder.Entity<User_Has_Job>()
.HasKey(t => new { t.UserId, t.JobId });
modelBuilder.Entity<User_Has_Job>()
.HasOne(am => am.job)
.WithMany(a => a.user_Has_jobs)
.HasForeignKey(am => am.JobId);
modelBuilder.Entity<User_Has_Job>()
.HasOne(am => am.user)
.WithMany(m => m.user_Has_jobs)
.HasForeignKey(am => am.UserId);
}
public DbSet<User> users { get; set; }
public DbSet<Job> jobs { get; set; }
public DbSet<User_Has_Job> user_Has_jobs { get; set; }
}
And this is the mapping i am currently using ;
public TodoMapping()
{
CreateMap<UserCreateDto, User>();
CreateMap<User, UserReadDto>().ForMember(
dto => dto.user_Has_jobs,
t => t.MapFrom(h => h.user_Has_jobs.Select(cs => job)));
CreateMap<User, UserUpdateDto>();
CreateMap<UserUpdateDto, User>();
CreateMap<JobUpdateDto, Job>();
CreateMap<Job, JobUpdateDto>();
CreateMap<JobCreateDto, Job>();
CreateMap<Job, JobReadDto>();
}
And these are my data transfer objects;
public class UserReadDto
{
[Key]
public int Id { get; set; }
[Required]
[MaxLength(250)]
public string Name { get; set; }
[Required]
[MaxLength(250)]
public string LastName { get; set; }
[Required]
[MaxLength(250)]
public string Mail { get; set; }
public List<JobReadDto> user_Has_jobs { get; set; }
public UserReadDto()
{
}
}
This is how i configure mapper ;
public void ConfigureServices(IServiceCollection services)
{
services.AddScoped<IUserRepo, SqlUserRepo>();
services.AddScoped<IJobRepo, SqlJobRepo>();
services.AddControllers();
services.AddDbContext<TodoApiContext>(
opt => opt.UseSqlServer(
Configuration.GetConnectionString("TodoApiConnection")));
services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies());
}
Even though there is a job recorded for the user with id 1;
DB screenshot
this is the output i am receiving after sending a GET request via postman ;
{
"id": 1,
"name": "s",
"lastName": "s",
"mail": "s",
"user_Has_jobs": []
}
As you can see "user_Has_jobs" is not null, but it is completely empty. I have tried changing the mapping a lot of times but so for it just doesn't work. Where do you think problem might be at ?
Thanks in advance for you time and help.
I have solved the problem. So it looks like i have to use Include() function and include the relation while trying to fetch results from the database like this ;
await _context.users.Include(u => u.user_Has_jobs).ThenInclude(s => s.job).FirstOrDefaultAsync(u => u.Id == addJobToUserDto.UserId);
if i use this code instead of fetching results directly and then map it to a dto, then each result object will have it's job as a list.

Multiply relationships ef core

I'm using entity core 3. I have 2 classes User and Ticket. User may have many Ticket's to me and many Ticket's from me. Ticket should have User-sender and User-receiver. I did so:
public class User
{
public int Id { get; set; }
public string PasswordHash { get; set; }
public string Email { get;
public ICollection<Ticket> TicketsToMe { get; set; }
public ICollection<Ticket> TicketsFromMe { get; set; }
}
public class Ticket
{
public int Id { get; set; }
public string Title { get; set; }
public int UserToId { get; set; }
public int UserFromId { get; set; }
public User UserTo { get; set; }
public User UserFrom { get; set; }
}
And I got the error: Unable to determine the relationship represented by navigation property 'Ticket.UserTo' of type 'User'. Either manually configure the relationship, or ignore this property using the '[NotMapped]' attribute or by using 'EntityTypeBuilder.Ignore' in 'OnModelCreating'.
Do you have any ideas?:)
You can use one of these
1 - Metadata. you can use InverseProperty to define the relationships.
if you use the metadata you should set UserToId and UserFromId to Nullable
public class User
{
public int Id { get; set; }
public string PasswordHash { get; set; }
public string Email { get; set; }
[InverseProperty("UserTo")]
public ICollection<Ticket> TicketsToMe { get; set; }
[InverseProperty("UserFrom")]
public ICollection<Ticket> TicketsFromMe { get; set; }
}
2 - FluentApi
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<User>()
.HasMany(a => a.TicketsFromMe)
.WithOne(a => a.UserFrom)
.HasForeignKey(a => a.UserFromId).OnDelete(DeleteBehavior.Restrict);
modelBuilder.Entity<User>()
.HasMany(a => a.TicketsToMe)
.WithOne(a => a.UserTo)
.HasForeignKey(a => a.UserToId).OnDelete(DeleteBehavior.Restrict);
}

Entity Framework Core Navigation Properties Error

I'm trying to make a simple app to try Entity Framework Core, but i a have problem with setting up relations between entities. My entities:
public class Card
{
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
public string Name { get; set; }
public string Surname { get; set; }
public string Adress { get; set; }
public DateTime DoB { get; set; }
public DateTime DoS { get; set; }
public User Portal { get; set; }
public List<Reservation> Res { get; set; }
}
public class Doctor
{
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
public string Name { get; set; }
public string Surname { get; set; }
public string Email { get; set; }
public TimeSpan Start_Working { get; set; }
public TimeSpan End_Working { get; set; }
public List<Reservation> Reservations { get; set; }
public int SpecID { get; set; }
public Spec Spec { get; set; }
}
public class Reservation
{
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
public DateTime DoR { get; set; }
public string Info { get; set; }
public int CardID { get; set; }
public Card Card_Nav_R { get; set; }
public int DoctorID { get; set; }
public Doctor Doctor { get; set; }
}
public class Spec
{
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
public string Name { get; set; }
public List<Doctor> Doctors { get; set; }
}
public class User
{
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
public string Email { get; set; }
public string Password { get; set; }
public int CardID { get; set; }
public Card Card { get; set; }
}
And a configuration class where i tried to set up relations:
class ApplicationContext:DbContext
{
public DbSet<User> Users { get; set; }
public DbSet<Card> Cards { get; set; }
public DbSet<Reservation> Reservations { get; set; }
public DbSet<Doctor> Doctors { get; set; }
public DbSet<Spec> Specs { get; set; }
public ApplicationContext()
{
Database.EnsureCreated();
}
protected override void OnModelCreating(ModelBuilder ModelBuilder)
{
ModelBuilder.Entity<User>().HasKey(u => u.Id);
ModelBuilder.Entity<Card>().HasKey(c => c.Id);
ModelBuilder.Entity<Doctor>().HasKey(d => d.Id);
ModelBuilder.Entity<Spec>().HasKey(s => s.Id);
ModelBuilder.Entity<Reservation>().HasKey(r => r.Id);
ModelBuilder.Entity<User>().Property(u => u.Email).IsRequired();
ModelBuilder.Entity<User>().Property(u => u.Password).IsRequired();
ModelBuilder.Entity<Card>().Property(c => c.Name).IsRequired();
ModelBuilder.Entity<Card>().Property(c => c.Surname).IsRequired();
ModelBuilder.Entity<Card>().Property(c => c.DoB).IsRequired();
ModelBuilder.Entity<Card>().Property(c => c.Adress).IsRequired();
ModelBuilder.Entity<Doctor>().Property(d => d.Name).IsRequired();
ModelBuilder.Entity<Doctor>().Property(d => d.Surname).IsRequired();
ModelBuilder.Entity<Doctor>().Property(d => d.Spec).IsRequired();
ModelBuilder.Entity<Doctor>().Property(d => d.Email).IsRequired();
ModelBuilder.Entity<Doctor>().Property(d => d.Start_Working).IsRequired();
ModelBuilder.Entity<Doctor>().Property(d => d.End_Working).IsRequired();
ModelBuilder.Entity<Reservation>().Property(r => r.Info).IsRequired();
ModelBuilder.Entity<Reservation>().Property(r => r.Card_Nav_R).IsRequired();
ModelBuilder.Entity<Reservation>().Property(r => r.Doctor).IsRequired();
ModelBuilder.Entity<Reservation>().Property(r => r.DoR).IsRequired();
ModelBuilder.Entity<Spec>().Property(s => s.Name).IsRequired();
ModelBuilder.Entity<Doctor>().HasOne<Spec>(d=>d.Spec).WithMany(s => s.Doctors).HasForeignKey(d => d.SpecID);
ModelBuilder.Entity<User>().HasOne<Card>(u => u.Card).WithOne(c => c.Portal).HasForeignKey<User>(u => u.CardID);
ModelBuilder.Entity<Reservation>().HasOne<Card>(r => r.Card_Nav_R).WithMany(c => c.Res).HasForeignKey(r => r.CardID);
ModelBuilder.Entity<Reservation>().HasOne<Doctor>(r => r.Doctor).WithMany(d => d.Reservations).HasForeignKey(r => r.DoctorID);
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlServer("Server=(localdb)\\mssqllocaldb;Database=Simple_Try;Trusted_Connection=True;");
}
}
So, when i tried to add migration or add something to database i saw this error:
System.InvalidOperationException: 'The property or navigation 'Spec' cannot be added to the entity type 'Doctor' because a property or navigation with the same name already exists on entity type 'Doctor'.'
I really don't know how to fix this, i tried to use annotations instead of Fluent API, but had the same result.
The cause of the exception is the following line:
ModelBuilder.Entity<Doctor>().Property(d => d.Spec).IsRequired();
because Doctor.Spec is a navigation property
public class Doctor
{
// ...
public Spec Spec { get; set; }
}
and navigation properties cannot be configured via Property fluent API.
So simply remove that line. Whether reference navigation property is required or optional is controlled via relationship configuration. In this case
ModelBuilder.Entity<Doctor>()
.HasOne(d => d.Spec)
.WithMany(s => s.Doctors)
.HasForeignKey(d => d.SpecID)
.IsRequired(); // <--
although the IsRequired is automatically derived from the FK property type - since SpecID is non nullable, then the relationship is required.
For more info, see Required and Optional Properties and Required and Optional Relationships documentation topics.

Conventions.Remove<OneToManyCascadeDeleteConvention>() in EF Core?

In my regular .NET Framework application, I was using EF 6.x and was also using some Inheritance, specifically:
PurchaseOrder.cs and SaleOrder.cs both inherit from Order.cs
And in the OnModelCreating() on my context class inheriting from IdentityDbContext, I was doing:
modelBuilder.Conventions.Remove<OneToManyCascadeDeleteConvention>();
This used to work, but now I am moving my application to .NET Core 2.0 and I am using EF Core. What achieves the same thing in EF Core? Because right now I am getting the error:
System.Data.SqlClient.SqlException (0x80131904): Introducing FOREIGN KEY constraint 'FK_Order_Business_CustomerId' on table 'Order' may cause cycles or multiple cascade paths. Specify ON DELETE NO ACTION or ON UPDATE NO ACTION, or modify other FOREIGN KEY constraints.
UPDATE
Here's the code after Ahmar's answer. In my context class, I have:
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
builder.HasDefaultSchema("PD");
builder.Entity<Customer>()
.HasMany(c => c.SaleOrders)
.WithOne(e => e.Customer)
.OnDelete(DeleteBehavior.SetNull);
builder.Entity<Supplier>()
.HasMany(po => po.PurchaseOrders)
.WithOne(e => e.Supplier)
.OnDelete(DeleteBehavior.SetNull);
builder.Entity<PurchaseOrder>()
.HasMany(li => li.LineItems)
.WithOne(po => po.PurchaseOrder)
.OnDelete(DeleteBehavior.SetNull);
builder.Entity<SaleOrder>()
.HasMany(li => li.LineItems)
.WithOne(po => po.SaleOrder)
.OnDelete(DeleteBehavior.SetNull);
}
And as far the Entities, they are:
public abstract class Business : IEntity
{
protected Business()
{
CreatedOn = DateTime.UtcNow;
}
public int Id { get; set; }
public string Name { get; set; }
public string TaxNumber { get; set; }
public string Description { get; set; }
public string Phone { get; set; }
public string Website { get; set; }
public string Email { get; set; }
public bool IsDeleted { get; set; }
public DateTime CreatedOn { get; set; }
public DateTime? ModifiedOn { get; set; }
public ICollection<Address> Addresses { get; set; } = new List<Address>();
public ICollection<Contact> Contacts { get; set; } = new List<Contact>();
}
[Table("Customers")]
public class Customer : Business
{
public decimal AllowedCredit { get; set; }
public decimal CreditUsed { get; set; }
public int NumberOfDaysAllowedToBeOnMaxedOutCredit { get; set; }
public ICollection<SaleOrder> SaleOrders { get; set; }
}
[Table("Suppliers")]
public class Supplier : Business
{
public ICollection<PurchaseOrder> PurchaseOrders { get; set; }
}
public abstract class Order : IEntity
{
protected Order()
{
Date = DateTime.UtcNow;
CreatedOn = DateTime.UtcNow;
}
public int Id { get; set; }
public DateTime Date { get; set; }
public decimal ShippingCost { get; set; }
public Currency ShippingCurrency { get; set; }
public decimal ShippingConversionRate { get; set; }
public bool IsDeleted { get; set; }
public DateTime CreatedOn { get; set; }
public DateTime? ModifiedOn { get; set; }
public ICollection<Invoice> Invoices { get; set; }
public ICollection<Note> Notes { get; set; }
}
[Table("PurchaseOrders")]
public class PurchaseOrder : Order
{
public int SupplierOrderNumber { get; set; }
public PurchaseOrderStatus Status { get; set; }
public decimal Vat { get; set; }
public decimal ImportDuty { get; set; }
public int SupplierId { get; set; }
public Supplier Supplier { get; set; }
public ICollection<PurchaseOrderLineItem> LineItems { get; set; }
}
[Table("SaleOrders")]
public class SaleOrder : Order
{
public decimal AmountToBePaidOnCredit { get; set; }
public SaleOrderStatus Status { get; set; }
public ICollection<SaleOrderLineItem> LineItems { get; set; }
public int CustomerId { get; set; }
public Customer Customer { get; set; }
}
So after doing what Ahmar suggested, I still get the same error when I do update-database.
You need to configure cascade delete behavior on each entity in .Net Core EF.
The Entity Framework Core Fluent API OnDelete method is used to specify the action which should take place on a dependent entity in a relationship when the principal is deleted.
The OnDelete method takes a DeleteBehavior enum as a parameter:
Cascade - dependents should be deleted
Restrict - dependents are
unaffected
SetNull - the foreign key values in dependent rows should
update to NULL
Example:
public class Company
{
public int Id { get; set; }
public string Name { get; set; }
public ICollection<Employee> Employees { get; set; }
}
public class Employee
{
public int Id { get; set; }
public string Name { get; set; }
public int? CompanyId { get; set; }
public Company Company { get; set; }
}
protected override void OnModelCreating(Modelbuilder modelBuilder)
{
modelBuilder.Entity<Company>()
.HasMany(c => c.Employees)
.WithOne(e => e.Company).
.OnDelete(DeleteBehavior.SetNull);
}
When deleting the Company, it will set CompanyId property in Employee table to null.
Get more detail at Configuring One To Many Relationships
PS. Please make sure your all referencing properties should be null able so, EF Core can set them null on delete. like CompanyId in about example.

Split table in Entity Framework into multiple entities

Here's the problem. I have table User which have quite a few fields. What I want to do is split this table into multiple entities like this:
User
-> GeneralDetails
-> CommunicationDetails
-> Address
etc.
All goes well when extracting some fields from User into GeneralDetails. However, when I try to do the same thing for CommunicationDetails EF blows up and require to establish one-to-one relationship between GeneralDetails and CommunicationDetails.
Sample entities definition:
public class User {
public int UserId { get; set; }
public string SomeField1 { get; set; }
public int SomeField2 { get; set; }
public virtual GeneralDetails GeneralDetails { get; set; }
public virtual CommunicationDetails CommunicationDetails { get; set; }
public virtual Address Address { get; set; }
}
public class GeneralDetails {
[Key]
public int UserId { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string Email { get; set; }
public virtual User User { get;set; }
}
public class CommunicationDetails {
[Key]
public int UserId { get; set; }
public string Phone { get; set; }
public string DeviceToken { get; set; }
public virtual User User { get;set; }
}
public class Address {
[Key]
public int UserId { get; set; }
public string City { get; set; }
public string Country { get; set; }
public string Street { get; set; }
public virtual User User { get;set; }
}
Sample mapping:
modelBuilder.Entity<User>().
HasRequired(user => user.GeneralDetails).
WithRequiredPrincipal(details => details.User);
modelBuilder.Entity<User>().
HasRequired(user => user.CommunicationDetails).
WithRequiredPrincipal(details => details.User);
modelBuilder.Entity<User>().
HasRequired(user => user.Address).
WithRequiredPrincipal(details => details.User);
modelBuilder.Entity<User>().ToTable("Users");
modelBuilder.Entity<GeneralDetails>().ToTable("Users");
modelBuilder.Entity<Address>().ToTable("Users");
Why on earth EF want this relationship? Is there any way this could be solved?
The correct way to actually do this is by Complex Types rather than entities. Its actually a more common problem than you think.
public class MyDbContext : DbContext
{
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelbuilder.ComplexType<CommunicationDetails>();
modelbuilder.ComplexType<GeneralDetails>();
modelbuilder.ComplexType<Address>();
modelbuilder.Entity<User>().ToTable("Users");
}
}

Categories