After reading a story about one-to-one foreign key associations and using this for a one-to-many association I was able to implement it with the following requirements:
- I can have many different classes with a property of the same type T
- All instances type T can be put in one table, even though the "owner" of this type is in different tables.
- A class can even have two properties of type T.
For instance: A customer may have a BillingAddress and a DeliveryAddress, both of type Address. Both addresses can be put in one table: Address.
public class Address
{
public int Id { get; set; }
public string Street { get; set; }
public string City { get; set; }
public string ZipCode { get; set; }
}
public class User
{
public int UserId { get; set; }
public string Name { get; set; }
public int BillingAddressId { get; set; }
public Address BillingAddress { get; set; }
public int DeliveryAddressId { get; set; }
public Address DeliveryAddress { get; set; }
}
public class MyDbContext : DbContext
{
public DbSet<Address> Addresses { get; set; }
public DbSet<User> Users { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<User>()
.HasRequired(p => p.DeliveryAddress)
.WithMany()
.HasForeignKey(p => p.DeliveryAddressId)
.WillCascadeOnDelete(false);
modelBuilder.Entity<User>()
.HasRequired(p => p.BillingAddress)
.WithMany()
.HasForeignKey(p => p.BillingAddressId)
.WillCascadeOnDelete(false);
}
}
The smart thing in this solution is that the Address does not have an "owning" user in it. So if I define a new class with an Address this address can be added to the same table of Address. So If I have ten different classes that all have an address I don't need ten address tables.
What to do if you have a collection of addresses?
Normally in a one-to-many relation the many side needs a foreign key to the one side plus a reference to the "owner":
An often seen example: blogs and posts: one blog has many posts. One post belongs to exactly one blog:
public class Blog
{
public int Id { get; set; }
public string Name { get; set; }
virtual public ICollection<Post> Posts {get; set;}
}
public class Post
{
public int Id { get; set; }
public string Text { get; set; }
public int BlogId { get; set; }
public Blog Blog { get; set; }
}
This naming will automatically lead to the correct one-to-many relationship, but if you want to specify in the DbContext:
public DbSet<Blog> Blogs { get; set; }
public DbSet<Post> Posts { get; set; }
and in OnModelCreating:
modelBuilder.Entity<Blog>()
.HasMany(b => b.Posts)
.WithRequired(post => post.Blog)
.HasForeignKey(post => post.BlogId);
Even if you would not need Post.Blog, you can't remove this property, because of the model creating. If you would remove it you would end up with magic strings to define the foreign key.
To be able to also have a collection of addresses (or in my original question: a lot of price histories, where each price history is a collection of prices) I combined these two methods.
public class Price
{
public int Id { get; set; }
public int PriceHistoryId { get; set; }
public virtual PriceHistory PriceHistory { get; set; }
public DateTime ActivationDate { get; set; }
public decimal Value { get; set; }
}
public class PriceHistory
{
public int Id { get; set; }
virtual public ICollection<Price> Prices { get; set; }
}
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
// Purchase Prices
public virtual PriceHistory PurchasePriceHistory { get; set; }
public int PurchasePriceHistoryId { get; set; }
// Retail prices
public virtual PriceHistory RetailPriceHistory { get; set; }
public int RetailPriceHistoryId { get; set; }
}
public class MyDbContext : DbContext
{
public DbSet<Product> Products { get; set; }
public DbSet<PriceHistory> PriceHistories { get; set; }
public DbSet<Price> Prices { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
// one price history has many prices: one to many:
modelBuilder.Entity<PriceHistory>()
.HasMany(p => p.Prices)
.WithRequired(price => price.PriceHistory)
.HasForeignKey(price => price.PriceHistoryId);
// one product has 2 price histories, the used method is comparable
// with the method user with two addresses
modelBuilder.Entity<Product>()
.HasRequired(p => p.PurchasePriceHistory)
.WithMany()
.HasForeignKey(p => p.PurchasePriceHistoryId)
.WillCascadeOnDelete(false);
modelBuilder.Entity<Product>()
.HasRequired(p => p.RetailPriceHistory)
.WithMany()
.HasForeignKey(p => p.RetailPriceHistoryId)
.WillCascadeOnDelete(false);
}
}
I've tested it with other classes that have several price histories:
- All prices will be in one table
- All price histories will be in one table
- Each reference to a price history needs a priceHistoryId.
If you look closely to the result it is in fact the implementation of a many-to-many relation where the price history is the coupling table.
I've tried to remove the PriceHistory class, and let a Product have several collections of Prices with a many-to-many in OnModelCreating, but that would lead to "Map" statements with magic strings, and separate tables for each PriceHistory.
Link to explanation about how to implement many-to-many