Entity Framework 6 Adding properties to join tables - c#

This is the scenario:
I have a products table and a categories table. The relationship is many-to-many: a category can have 1 or more products....and a product can be in 1 or more categories...
The Code-First mapping looks like this....
public class Product
{
//...additional properties...
public virtual ICollection<Category> AssociatedCategories {get; set;}
}
public class Category
{
//...additional properties...
public virtual ICollection<Product> AssociatedProducts {get; set;}
}
Now, under the hood, entity framework will create a join table called ProductCategory with columns ProductID and CategoryID. That's great....
Here's the thing though, I need to introduce a sort order...basically just a cardinal positioning index, but this number exists only at the part in the relationship where product and category meet each other. For example, a product X might have a sort order value of "5" in Category Y, but that some product--X--could have a different sort value--say 10--in Category Z.
Naturally, I could create an entity specifically for this type of thing...but it would require a new table be made...there would be 3 columns for the Category ID, Product ID, and sort order. What I'd really like to be able to do is tap into the table that entity framework already made....it will already keep track of products IDs and category IDs in the join table....is there any way to make use of the table that already exists?

You need to create a specific entity for the join table in order to do this.
public class Product
{
//...additional properties...
public virtual ICollection<ProductCategoryXref> AssociatedCategories {get; set;}
}
public class Category
{
//...additional properties...
public virtual ICollection<ProductCategoryXref> AssociatedProducts {get; set;}
}
public class ProductCategoryXref
{
public int ProductId { get; set; }
public int CategoryId { get; set; }
public int SortOrder { get; set; }
// Additional Columns...
public virtual Product Product { get; set; }
public virtual Category Category { get; set; }
}
If you are using the Fluent API to configure your entities it will look something like this:
public class ProductCategoryXrefMap : EntityTypeConfiguration<ProductCategoryXref>
{
ProductCategoryXrefMap()
{
HasKey(pk => new { pk.ProductId, pk.CategoryId });
HasRequired(p => p.Product).WithMany(p => p.AssociatedCategories).HasForeignKey(fk => fk.ProductId);
HasRequired(p => p.Category).WithMany(p => p.AssociatedProducts).HasForeignKey(fk => fk.CategoryId);
ToTable("ProductCategoryXref");
}
}

Related

Remove composite primary key for the join table created by entity framework

I have two classes, Product and Invoice, with many to many relationship between them
Entity framework has created three tables for me, Products, Invoices, and a join table called, InvoiceProducts (or something like that)
As I have learned, by default, entity framework creates a composite primary key on the fields InvoiceId and ProductId in the join table, so together, those foreign keys, must be unique.
Well, this is an issue, cause sometimes a person wants to order two of the same item, in the same invoce.
I saw this answer, but it only helps if you are creating the join table in C# as well. But when using entity framework, I no longer create a join table, I just Create my classes as the following:
public class Invoice
{
public int Id { get; set; }
public string InvoiceNumber { get; set; }
public double TotalPrice { get; set; }
public List<Product> Products { get; set; }
}
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public double Price { get; set; }
public List<Invoice> Invoices { get; set; }
}
public class DB : DbContext
{
public DB() : base("MyConnectionstringName") { }
public DbSet<Invoice> Invoices { get; set; }
public DbSet<Product> Products { get; set; }
}
So, any thoughts?
adding a quantity column seems architecturally wrong to me, doesn't it to you?
No, but it's your data model. If you want that then the model would be something like
public class InvoiceLine
{
public int InvoiceId { get; set; }
public int InvoiceLineId { get; set; }
public int ProductId{ get; set; }
public virtual Invoice Invoice { get; set; }
public virtual Product Product { get; set; }
}
Configured like this:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Invoice>()
.HasMany(i => i.Products)
.WithMany(p => p.Invoices)
.UsingEntity<InvoiceLine>(
e => e.HasOne( il => il.Product)
.WithMany()
.HasForeignKey(il => il.ProductId),
e => e.HasOne(il => il.Invoice)
.WithMany()
.HasForeignKey(il => il.InvoiceId),
e => e.HasKey(e => new { e.InvoiceId, e.InvoiceLineId })
);
base.OnModelCreating(modelBuilder);
}
Note how you still have a Many-to-Many and don't have to directly deal with the InvoiceLine entities. And it creates the table like
CREATE TABLE [InvoiceLine] (
[InvoiceId] int NOT NULL,
[InvoiceLineId] int NOT NULL,
[ProductId] int NOT NULL,
CONSTRAINT [PK_InvoiceLine] PRIMARY KEY ([InvoiceId], [InvoiceLineId]),
CONSTRAINT [FK_InvoiceLine_Invoice_InvoiceId] FOREIGN KEY ([InvoiceId]) REFERENCES [Invoice] ([Id]) ON DELETE CASCADE,
CONSTRAINT [FK_InvoiceLine_Product_ProductId] FOREIGN KEY ([ProductId]) REFERENCES [Product] ([Id]) ON DELETE CASCADE
);
CREATE INDEX [IX_InvoiceLine_ProductId] ON [InvoiceLine] ([ProductId]);
This is the default, convenient mapping for many-to-many tables where you just want to associate the two tables with one another. Something like what you describe could involve adding something like a "Quantity" to the InvoiceProducts table. (rather than duplicating the same ProductId+InvoiceId combination)
This would involve adding an InvoiceProduct entity:
public class InvoiceProduct
{
[Key, ForeignKey(nameof(Invoice), Column(Order=0)]
public int InvoiceId { get; set; }
[Key, ForeignKey(nameof(Product), Column(Order=1)]
public int ProductId { get; set; }
public int Quantity { get; set; }
public virtual Invoice Invoice { get; set; }
public virtual Product Product { get; set; }
}
The navigation properties in your Invoice and Product entities need to be changed to InvoiceProducts rather than Products and Invoices respectively. From there you can still access the respective collection, but it's a little less convenient because the collection will be typed to InvoiceProduct which allows you to get to the Quantity column for the relationship. For instance to list the Products for an invoice you would need to use invoice.InvoiceProducts.Select(x => x.Product); rather than invoice.Products.
The default many-to-many mapping is more convenient, but limited to just representing the relationship. Mapping the joining table entity gives you more flexibility for requirements like this.
Edit: The alternative is to replace the Many-to-Many with a One-to-Many-to-One relationship using a Joining table that has a dedicated PK column w9th the FKs to each table.
For instance:
public class InvoiceProduct
{
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int InvoiceProductId { get; set; }
[ForeignKey(nameof(Invoice), Column(Order=0)]
public int InvoiceId { get; set; }
[ForeignKey(nameof(Product), Column(Order=1)]
public int ProductId { get; set; }
public virtual Invoice Invoice { get; set; }
public virtual Product Product { get; set; }
}
Personally, I would prefer the Quantity column where the combination forms the PK. Using this approach looking through the data honestly looks like a bug. When you look at something like an invoice that lists products, these are typically presented as a Product, a price, a quantity, and a total. If I order 10 instances of the same product, I expect to see the product with a Qty of 10 and a total rather than 10 line items for the same product. There is nothing architecturally wrong from a data schema or EF implementation using a Quantity field. The same approach is needed if you want to use something like a Soft-delete associations when removing product-invoice associations. (I.e. an IsActive rather than deleting the joining row) This would require defining an InvoiceProduct entity with the IsActive flag in the same way. (Same for tracking things like CreatedBy/Date, ModifiedBy/Date, etc.)

Create many-to-many relationship between two entities via multiple columns

I have the following four tables:
Customer
Id
FirstName
...
ConsumptionPoint
Id
Address
...
Invoice
Id
InvoiceNumber
CustomerId
ConsumptionPointId
...
ContractAccount
Id
ContractAccountNumber
CustomerId
ConsumptionPointId
IsCurrentDelivery
...
I want to get the ContractAccountNumber for an Invoice.
Is it possible to create some kind of a relation between these both to access the ContractAccount(s) of an Invoice directly?
Currently I'm doing something like:
invoice.Customer.ContractAccounts
.Where(ca => ca.ConsumptionPoint == invoice.ConsumptionPoint &&
ca.IsCurrentDelivery == true).FirstOrDefault();
update in SQL I would simply do a join with multiple conditions:
SELECT i.Id AS InvoiceId, ca.Id AS ContractAccountId,
ca.ContractAccountNumber
FROM Invoices i
LEFT JOIN ContractAccounts ca
ON i.ConsumptionPointId = ca.ConsumptionPointId
AND i.CustomerId = ca.CustomerId
WHERE ca.IsCurrentDelivery = 1
update 2:
Basically I just want to get rid of the ca.ConsumptionPoint == invoice.ConsumptionPoint in the Where-Clause and want to define this inside the relation.
Actually this is a many-to-many relationship: one Invoice can link to many ContractAccounts (via different Customer/ConsumptionPoint combinations) and one ContractAccount can link to many Invoices. Is there no way to tell .net to build a many-to-many relationship, based on the combination of two custom columns?
I do not think that you need two keys for that. Using EF core you can create a relationship like that:
Invoice:
public class Invoice
{
public int Id {get; set;}
public int InvoiceNumber {get; set;}
public int CustomerId {get; set;}
public Customer Customer {get; set;}
public int ConsumptionPointId {get; set;}
public int ContractAccountId {get; set;}
public ContractAccount ContractAccount {get; set;}
}
public class ContractAccount
{
public int Id {get; set;}
public int ContractAccountNumber {get; set;}
public bool IsCurrentDelivery {get; set;}
}
Then configuration:
modelBuilder.Entity<Invoice>()
.HasOne(b => b.ContractAccount)
.WithOne()
.HasForeignKey<Invoice>(b => b.ContractAccountId);
//probably already exists in your code
modelBuilder.Entity<Invoice>()
.HasOne(b => b.Customer )
.WithMany()
.HasForeignKey(b => b.CustomerId);
And then you can access it directly from invoice: invoice.ContractAccount
After some deeper investigation I have found a solution.
Basically it's just a many-to-many relationship. So we can add the appropriate properties to Invoice and ContractAccount Model:
public class Invoice
{
[Key]
public long Id { get; set; }
public Customer Customer { get; set; }
public ConsumptionPoint ConsumptionPoint { get; set; }
public virtual ICollection<ContractAccount> ContractAccounts { get; set; }
}
public class ContractAccount
{
[Key]
public long Id { get; set; }
public Customer Customer { get; set; }
public ConsumptionPoint ConsumptionPoint { get; set; }
public ICollection<Invoice> Invoices { get; set; }
}
Now, we just have to configure the relationship manually:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<Invoice>()
.HasMany(i => i.ContractAccounts)
.WithOne()
.HasForeignKey(ca => new { ca.CustomerId, ca.ConsumptionPointId })
.HasPrincipalKey(i => new { i.CustomerId, i.ConsumptionPointId });
modelBuilder.Entity<ContractAccount>()
.HasMany(ca => ca.Invoices)
.WithOne()
.HasForeignKey(i => new { i.CustomerId, i.ConsumptionPointId })
.HasPrincipalKey(ca => new { ca.CustomerId, ca.ConsumptionPointId });
}
And that's it. Now I can do something like:
invoice.ContractAccounts
.Where(ca => ca.IsCurrentDelivery == true).FirstOrDefault();
which is way better than before.
Thanks for your comments, which pointed me to the right direction.

1 to many relationship isn't resulting in what I'd expect

In a scenario where a product can have many categories I'm trying to create this using code-first, I have a Product model which has a collection of Category like so:
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public ICollection<Category> Categories { get; set; }
}
My Category looks like:
public class Category
{
public Category(CategoryEnum #enum)
{
Id = (int)#enum;
Name = #enum.ToString();
}
public Category() { }
[Key, DatabaseGenerated(DatabaseGeneratedOption.None)]
public int Id { get; set; }
public string Name { get; set; }
public static implicit operator Category(CategoryEnum #enum) => new Category(#enum);
public static implicit operator CategoryEnum(Category category) => (CategoryEnum)category.Id;
}
Creating a migration then updating the database doesn't create a new table for this to join the 2 (Product and Category) together. I'd expect a new table with a ProductId and CategoryId but this isn't the case. Instead, it just creates a ProductId in the Category table?
How do I make EF create a table which joins the 2 together without having to create a new DbSet<T>
That is how it's supposed to be.
Example:
Product 1 Chicken
Categorie 1 Meat idProduct = 1
Categorie 2 Healthy idProduct = 1
etc...
You don't need another table for a relation one to many. The table is only usefull for many to many

How to fetch one column from another table using Fluent NHibernate

I have a Products table:
ProductId
ProductDescription
CategoryId
And a Categories table:
CategoryId
CategoryDescription
***For every product, I would like to display a line like so:
Product Id | Product Description | Category Description
I have not been successful in forming the necessary mapping that is required for the above task.
Products Mapping I am using:
public ProductsMap()
{
Table("Products");
Id(x => x.ProductId);
Map(x => x.ProductDescription);
Map(x => x.CategoryId);
References(x => x.Categories)
.Column("CategoryId")
.Not.Nullable();
// Need Join() statement here?
...
My Products class:
public class Products
{
public virtual int ProductId { get; set; }
public virtual string ProductDescription { get; set; }
public virtual int CategoryId { get; set; }
public virtual Category Category { get; set; }
public virtual int? CategoryDescription { get; set; } // Not in the db table.
}
My goal is to have the CategoryDescription field in the above class to be populated automatically by Fluent-NHibernate through the mapping specified.
I used the join statement suggested by this answer but I got various exceptions for the following statement:
List<Products> products = session.Query<Products>().ToList();
Note: I can pull in all products from the database without the corresponding column in the Categories table, so I know that my database connectivity is good, and that the basic functionality of the application is sound.
I am new to Fluent-NHibernate, have invested quite a bit of time on this, but feel I am not getting anywhere. I would appreciate some directed guidance.
I'm a little confused because you seem to mixing singular and plural, but I would create separate domain mappings for the product and category
public class Product
{
public virtual int ProductId { get; set; }
public virtual string ProductDescription { get; set; }
public virtual Category Category { get; set; }
}
public class Category
{
public virtual int CategoryId { get; set; }
public virtual string CategoryDescription { get; set; }
}
map them the way you are mapping in the question, then create a view model
public class ProductViewModel
{
public virtual int ProductId { get; set; }
public virtual string ProductDescription { get; set; }
public virtual string CategoryDescription { get; set; }
}
that gets populated with this query
var products = session.Query<Products>().Select(p => new ProductViewModel()
{
ProductId = p.ProductId,
ProductDescription = p.ProductDescription,
CategoryDescription = p.Category.CategoryDescription
});
This will produce a query that only returns the columns you need. If you return full entities, you are going to return information you don't need.

entity framework - many to many relationship

Hi I try use Many to Many relationship with EF Fluent API. I have 2 POCO classes.
public class Project
{
public int ProjectId { get; set; }
public virtual ICollection<Author> Authors { get; set; }
public Project()
{
Authors = new List<Author>();
}
}
public class Author
{
public int AuthorId { get; set; }
public virtual ICollection<Project> Projects { get; set; }
public Author()
{
Projects = new List<Project>();
}
}
And I map many to many relationship with this part of code:
////MANY TO MANY
modelBuilder.Entity<Project>()
.HasMany<Author>(a => a.Authors)
.WithMany(p => p.Projects)
.Map(m =>
{
m.ToTable("ProjectAuthors");
m.MapLeftKey("ProjectId");
m.MapRightKey("AuthorId");
});
This created table ProjectsAuthors in DB. It is my first attempt with this case of relationship mapping.
If I omitted this mapping it created table AuthorProject with similar schema. It is correct bevahior?
By trial and error I found the following. Given two classes...
public class AClass
{
public int Id { get; set; }
public ICollection<BClass> BClasses { get; set; }
}
public class BClass
{
public int Id { get; set; }
public ICollection<AClass> AClasses { get; set; }
}
...and no Fluent mapping and a DbContext like this...
public class MyContext : DbContext
{
public DbSet<AClass> AClasses { get; set; }
public DbSet<BClass> BClasses { get; set; }
}
...the name of the created join table is BClassAClasses. If I change the order of the sets...
public class MyContext : DbContext
{
public DbSet<BClass> BClasses { get; set; }
public DbSet<AClass> AClasses { get; set; }
}
...the name of the created join table changes to AClassBClasses and the order of the key columns in the table changes as well. So, the name of the join table and the order of the key columns seems to depend on the order in which the entity classes are "loaded" into the model - which can be the order of the DbSet declarations or another order if more relationship are involved - for example some other entity refering to AClass.
In the end, it doesn't matter at all, because such a many-to-many relationship is "symmetric". If you want to have your own name of the join table, you can specify it in Fluent API as you already did.
So, to your question: Yes, naming the join table AuthorProjects is correct behaviour. If the name had been ProjectAuthors it would be correct behaviour as well though.

Categories

Resources