I have an order class with has a collection of products...
public class Order
{
int id { get; set; }
public virtual ICollection<Product> Products { get; set; }
}
public abstract class Product
{
int id { get; set; }
}
Derived from product, I have several different types of product...
public class ProductA : Product
{
public string CustomFieldA {get; set;}
}
public class ProductB : Product
{
public string CustomFieldB {get; set;}
}
I can add these derived products to the order and save them to the database...
var producta = new ProductA();
producta.CustomFieldA = "abc";
order.Products.Add(product);
order.SaveChanges();
This updates the database correctly.
What I can't then figure out is how I access this data!
Order.Products just contains the base class, how can I check which type of derived class each record in Order.Products is and access, for example, CustomFieldA of any that are ProductA.
I suspect either I'm missing something really obvious, or I've done the first part completely wrong!
Related
I have a question concerning EF Core 2.1
I have a base type, let's name it Customer, from which CustomerOld and CustomerNew are derived. These are automatically stored in one table, so far so good.
Now I have a generic type for mapping Customer to Product :
public class CustomerToProduct<T> where T : Customer
{
public int CustomerId { get; set; }
public T Customer { get; set; }
public int ProductId { get; set; }
public Product Product { get; set; }
...
}
The derived types of CustomerToProduct have no specific properties. I just want to use them such as the Customer I access through the property CustomerToProduct.Customer is of the derived type.
I defined the DbSets for the derived types in my DbContext as follows, which of course leads to separate tables:
public class MyDbContext : DbContext
{
public DbSet<OldCustomerToProduct> OldCustomerToProducts { get; set; }
public DbSet<NewCustomerToProduct> NewCustomerToProducts { get; set; }
...
}
How do I store all derived Types of CustomerToProduct<T> in the same table? How do I define the DbSets<>?
Yours looks like a good candedate for the mapping pattern TPH (Type per Hierarchy).
You can read more here about the topic.
Is there a better way to accomplish this end-goal of having easily-queryable (and Include-able) cross-sections of a related many-to-many entity stored in the same table?
I started off without implementing TPH in the join table, but that makes consuming one type or another in queries more involved, afaict.
// table Related: [Id]
public class Related
{
public Guid Id { get; set; }
public List<RelatedOther> RelatedOthers { get; set; } = new List<RelatedOther>();
public List<RelatedOtherOne> RelatedOtherOnes { get; set; } = new List<RelatedOtherOne>();
public List<RelatedOtherTwo> RelatedOtherTwos { get; set; } = new List<RelatedOtherTwo>();
}
// table RelatedOther: [RelatedId, OtherId, Type]
public abstract class RelatedOther
{
public Guid RelatedId { get; set; }
public Guid OtherId { get; set; }
public Related Related { get; set; }
public Other Other { get; set; }
public abstract RelatedOtherType Type { get; }
}
public class RelatedOtherOne : RelatedOther
{
public override RelatedOtherType Type => RelatedOtherType.One;
// should be unnecessary, 'Other' should be correct type
public OtherOne OtherOne { get; set; }
}
public class RelatedOtherTwo : RelatedOther
{
public override RelatedOtherType Type => RelatedOtherType.Two;
// should be unnecessary, 'Other' should be correct type
public OtherTwo OtherTwo { get; set; }
}
public enum RelatedOtherType : int
{
One = 1,
Two = 2
}
// table Other: [Id, OneProp, TwoProp]
public abstract class Other
{
public Guid Id { get; set; }
public List<RelatedOther> RelatedOthers { get; set; } = new List<RelatedOther>();
}
public class OtherOne : Other
{
public string OneProp { get; set; }
}
public class OtherTwo : Other
{
public string TwoProp { get; set; }
}
TPH is mapped like this
M2M is mapped like this + discriminator in HasKey()
This gets even more complicated (if not impossible?) when the 'Related' entity evolves into a TPH strategy like the 'Other'.
I have no easy solution but as I stumbled across the same problem I thought I'll share what I have so far.
I found out that I usually need to load all or many types of the relations to the classes of a TPH structure.
So I use the base many-to-many class to load the related objects. Thus this class cannot be abstract:
public class Event2Location
{
[Required]
public Event Event { get; set; }
public int EventId { get; set; }
[Required]
public Location Location { get; set; }
public int LocationId { get; set; }
public byte EntityType { get; set; }
}
The derived class only adds some properties for easier access:
public class Event2Country : Event2Location
{
[NotMapped]
public Country Country
{
get { return base.Location as Country; }
set { base.Location = value; }
}
[NotMapped]
public int CountryId
{
get { return base.LocationId; }
set { base.LocationId = value; }
}
}
In the Event class I have:
public virtual ICollection<Event2Location> Event2Locations { get; set; }
[NotMapped]
public virtual ICollection<Event2Country> Event2Countries => Event2Locations?.OfType<Event2Country>().ToList();
// I should probably add some caching here if accessed more often
[NotMapped]
public virtual ICollection<Event2City> Event2Cities => Event2Locations?.OfType<Event2City>().ToList();
So when I load the joined tables I can use
.Include(e => e.Event2Locations).ThenInclude(j => j.Location)
And I can access the relations of a specific type as needed with the NotMapped Collections.
I still use the derived Event2... classes to add a new relationship.
As you see I have added a column EntityType to the many-to-many class which I use as TPH discriminator. With this column I can also declare which types of Relations/entities I want to load if I do not want to load all.
modelBuilder.Entity<Event2Location>()
.HasDiscriminator<byte>("EntityType")
.HasValue<Event2Location>(0)
.HasValue<Event2Country>(1)
This is surely far from perfect but I finally gave up on optimizing that. First EFCore has to become more mature. Second I want to see how I actually use these structures.
PS: Actually my Location TPH structure has parent-child-relationships within it. Here I did not create a TPH structure for the relation class (as you said - not possible or at least not reasonable). I added ParentType and ChildType. Thus I can determine which relations I actually want to load. Then I fetch the related Locations of the types I need manually on the client side from the result.
I currently have a MVC5 site with a TPH relationship with classes as follows:
public abstract class product{
public int productID {get;set;}
}
public class toy : product {
public virtual List<ChildComment> Comments {get;set;}
public virtual List<AdultComment> Comments {get;set;}
}
public class tool : product {
public virtual List<AdultComment> Comments {get;set;}
}
public class ChildComment {
public CommentID {get;set;}
public string commentText {get;set;}
public virtual product Product {get;set;}
}
public class AdultComment {
public CommentID {get;set;}
public string commentText {get;set;}
public virtual product Product {get;set;}
}
My Issue is when:
1) I am creating a new adult comment in the adult comment controller
2) I use db.Products.find(id) to add a product to the product virtual property of the comment
3) I go to the view of the Product I just added the comment to and see that there are 0 comments (lets say I tried to add a comment to a toy, but remember I didn't cast it as a toy when I added it to the virtual property)
4) When I go to the database, there are 3 key columns in the adultcomment table: one for product, one for toys, and one for tools. The correct id was placed in the product column and the others are null
Do I have to cast a product as either a toy or tool before adding it to the adultcomment's virtual property?
Why are there extra columns in the adultcomment table, is it possible to consolidate to one single id column (since after all, i have one products table in my tph), and should I do so if it is possible?
Add the foreignKey attribute to new Comment class
public class Comment
{
[Key]
public int CommentID { get; set; }
public string commentText { get; set; }
public int productID { get; set; }
[ForeignKey("productID")]
public virtual Product Product { get; set; }
}
so AdultComment now looks like this
public class AdultComment : Comment
{
}
you will have to add a unique Identifier when creating a new product despite database auto generating id
using (var context = new YOUR-CONTEXT())
{
var toy = new Toy
{
productID = 1, //Unique identifier
AdultComments = new List<AdultComment>()
{
new AdultComment { commentText = "Some comment" }
}
};
context.Products.Add(toys);
context.SaveChanges();
}
public class Link
{
public virtual Category Category { get; set; }
}
public class Category
{
public Category(){Links = new List<Link>}
public virtual IList<Link> Links {get; set;}
}
My problem is when I wanna create Link with reference to Category I got a error with foreign key.
I try to use fluent api, get threw repo category but still the same problem.
EF want to create new object of Category.
To pass data I get to controller second repo(category) and pass to viewbag data.
4 hours spent to find solution but still I've got nothing, could someone explain it to me how it work that I cant pass that :/
Both of your classes need properties that can be mapped to primary keys. This is most commonly done with an int called ID...
public class Link
{
public int ID { get; set; }
public virtual Category Category { get; set; }
}
public class Category
{
public int ID { get; set; }
public Category(){Links = new List<Link>}
public virtual IList<Link> Links {get; set;}
}
With Entity Framework, if I have the following model:
class Asset {
public int Id { get; set; }
public string Name { get; set; }
public virtual ICollection<AssetGroup> Groups { get; set; }
}
class AssetGroup {
public int Id { get; set; }
public string Name { get; set; }
public virtual ICollection<Asset> Assets { get; set; }
}
Adding an existing asset to a given group without having to query the database to load the asset in question is relatively straightforward:
using(var context = new MyContext()) {
AssetGroup assetGroup = context.AssetGroups.Find(groupId);
// Create a fake Asset to avoid a db query
Asset temp = context.Assets.Create();
temp.Id = assetId;
context.Assets.Attach(temp);
assetGroup.Assets.Add(temp);
context.SaveChanges();
}
However, I now have the problem that I have extended the model so that there are multiple Asset types, implemented as an inheritance hierarchy. In the process, the Asset class has been made abstract, since an asset with no specific type makes no sense:
abstract class Asset {
public int Id { get; set; }
public string Name { get; set; }
public virtual ICollection<AssetGroup> Groups { get; set; }
}
class SpecificAsset1 : Asset {
public string OneProperty { get; set; }
}
class SpecificAsset2 : Asset {
public string OtherProperty { get; set; }
}
The problem is that now the Create() call throws an InvalidOperationException because "Instances of abstract classes cannot be created", which of course makes sense.
So, the question is, how can I update the many-to-many relation without having to go fetch the Asset from the database?
You can use the generic Create<T> method of DbSet<T> to create a derived entity object.
var temp1 = context.Assets.Create<SpecificAsset1>();
and continue from here using temp1 as a stub entity the way you already did.