6

Simplified: In my DB I have a product that was sold with different prices on different dates. In other words it has a Price History. I have two classes: Product and Price with a one-to-many relationship:

public class Product
{
    public int ProductId {get; set;}
    public string Name {get; set;}

    public ICollection<Price> Prices {get; set;}
}

public class Price
{
    public int PriceId {get; set;}

    // foreign key to Product:
    public int ProductId {get; set;}
    public Product Product {get; set;}

    public DateTime ActivationDate {get; set;}
    public decimal value {get; set;}
}

public class MyDbContext : DbContext
{
    public DbSet<Price> Prices { get; set; }
    public DbSet<Product> Products { get; set; }
}

So far so good, Entity Framework knows how to handle this. With the use of these two classes I am able to get the price of a certain product on a certain date.

But what if my product has two price histories: a Purchase Price history and a Retail Price History?

public class Product
{
    public int ProductId {get; set;}
    public string Name {get; set;}

    public ICollection<Price> PurchasePrices {get; set;}
    public ICollection<Price> RetailPrices {get; set;}  
}

Because these two collections are to the same type I don't want separate tables filled with object of the same type (the real reason: I have a lot of classes with price collections).

So I have to do a bit of coding using Fluent API. My gut feeling says I need joining tables, like in a many-to-many relationship, mayby using the ManyToManyNavigationPropertyConfiguration.Map.

How to do this?

Harald Coppoolse
  • 28,834
  • 7
  • 67
  • 116
  • 1
    Not sure what you are implementing but i think you should have another property on your Price entity to differentiate between a sales price and purchase price. maybe a property of type boolean will help. – Ziregbe Otee Feb 18 '16 at 09:03
  • 1
    *"I don't want separate tables"* Then you need discriminator column in your table, i.e. TPH inheritance strategy. See [How to choose an Inheritance Strategy](http://blogs.msdn.com/b/alexj/archive/2009/04/15/tip-12-choosing-an-inheritance-strategy.aspx) – Ivan Stoev Feb 18 '16 at 12:01

3 Answers3

4

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

Harald Coppoolse
  • 28,834
  • 7
  • 67
  • 116
  • Thank you very much for the detailed explanation of your setup. I had a similar problem with a collection of related classes and your post helped me a lot. – dizarter Apr 27 '22 at 00:05
  • This is a great explanation of an elegant solution. I'm facing a similar problem and have been wracking m brain for the best approach, and I think this is it,. Thanks! – Matt G Mar 19 '23 at 04:10
1

At the moment your code is working because of the EF naming convention:

Code First infers that a property is a primary key if a property on a class is named “ID” (not case sensitive), or the class name followed by "ID". If the type of the primary key property is numeric or GUID it will be configured as an identity column.

EF sees you have a one-to-many so it automatically takes ProductId as the foreign key. If you want to define multiple collections of the same entity you'll have to define your foreign keys manually.

public class Price
{
   public int PriceId {get; set;}

   public int ProductPurchaseId {get; set;}
   public Product ProductPurchase {get; set;}

   public int ProductRetailId {get; set;}
   public Product ProductRetail {get; set;}

   public DateTime ActivationDate {get; set;}
   public decimal value {get; set;}
}

And in fluent api:

modelBuilder<Product>().HasMany(p => p.PurchasePrices)
                       .WithRequired(p => p.ProductPurchase)
                       .HasForeignKey(p => p.ProductPurchaseId);

modelBuilder<Product>().HasMany(p => p.RetailPrices)
                       .WithRequired(p => p.ProductRetail)
                       .HasForeignKey(p => p.ProductRetailId);

This of course means you need to have 2 foreign keys to Product in your Price table.

Alexander Derck
  • 13,818
  • 5
  • 54
  • 76
  • This was indeed my first solution. However I found that I have 6 different classes, each with a price history. That would result in one table with 6 foreign keys where five of them are null, or 6 different tables with price histories. Therefore I was looking in a solution similar to a coupling table: one side the Id of the Price, the other side the Id of the class that owns the price history. However, that still lead to one coupling table per class that holds a price history – Harald Coppoolse Feb 19 '16 at 15:54
0

As per my understanding of your requirement you need one extra field in your price table which will tell you what type of price you are going to store. For example:

public class Price
{
    public int PriceId {get; set;}

    // The PriceType will recognise among different type of price- Sell Price,          Purchase Price etc.

    public string PriceType{get;set;}

    // foreign key to Product:
    public int ProductId {get; set;}
    public Product Product {get; set;}

    public DateTime ActivationDate {get; set;}
    public decimal value {get; set;}
}
  • This would indeed be a solution, but I have 6 different classes, each with a price history. That would result in one table with 6 foreign keys where five of them are null, or 6 different tables with price histories – Harald Coppoolse Feb 19 '16 at 15:51