Sharing a table using Entity Framework Core - c#

I have multiple entities that I would like to share a single "Images" table. For example, products can have a list of images and categories can have a list of images. I would like to use the enum "EntityType" to distinguish what type of entity it is. My solution below doesn't work because there is a foreign key error when I try to insert an image with a EntityId that might exist in Category but not in Product. This makes sense because the solution below isn't taking into account the "EntityType". Are there any recommendations for how I can accomplish this? I know I can use "ProductId", "CategoryId", etc instead of "EntityId" but I will have a lot of entities so I would prefer to not to do it that way.
public class Product
{
public int Id { get; set; }
public List<Image> ProductImages { get; set; }
}
public class Category
{
public int Id { get; set; }
public List<Image> CategoryImages { get; set; }
}
public class Image
{
public int EntityId { get; set; }
public EntityType EntityType { get; set; }
public string ImageUrl { get; set; }
public Product Product { get; set; }
public Category Category { get; set; }
}
modelBuilder.Entity<Product>().ToTable("Product");
modelBuilder.Entity<Category>().ToTable("Category");
modelBuilder.Entity<Image>().ToTable("Image");
modelBuilder.Entity<Image>().HasOne(p => p.Product).WithMany(p => p.ProductImages).HasForeignKey(p => p.EntityId);
modelBuilder.Entity<Image>().HasOne(p => p.Category).WithMany(p => p.CategoryImages).HasForeignKey(p => p.EntityId);

What you're describing is a many-to-many relationship. For that, you'll need an entity to track said relationship:
public class ProductImage
{
[ForeignKey(nameof(Product))]
public int ProductId { get; set; }
public Product Product { get; set; }
[ForeignKey(nameof(Image))]
public int ImageId { get; set; }
public Image Image { get; set; }
}
On your Product/Category classes:
public ICollection<ProductImage> ProductImages { get; set; }
Then, for your fluent config:
modelBuilder.Entity<ProductImage>().HasOne(p => p.Product).WithMany(p => p.ProductImages);
modelBuilder.Entity<ProductImage>().HasOne(p => p.Image).WithMany();
Do the same with your categories.

Related

How to save collection of foreign keys in code first

Update: At this moment I can assign only one product to Recipe. What I want to do is add access to all products from db in recipe (controller create) - here Im using public int ProductId but it allow to save only one product. I want to choose a few products from this list and save foreign keys in database. photo
I also tried add public List < int > in ProductId but I got error from entity framework.
I will be grateful for any help.
public class Recipe
{
[Key]
public int Id { get; set; }
[Required]
public string Name { get; set; }
[Required]
public int ProductId { get; set; }
public List<Product> Products { get; set; }
public Recipe()
{
this.Products = new List<Product>();
}
}
public class Product
{
[Key]
public int Id { get; set; }
[Required]
public string Name { get; set; }
[Required]
public Recipe? Recipes { get; set; }
}
If you want to create a one-to-many relationship you are almost in the correct direction, but you should remove the public int ProductId { get; set; } and re-arrange as like as below example.
Say you have the following classes:
public class Recipe
{
[Key]
public int Id { get; set; }
[Required]
public string Name { get; set; }
public List<Product> Products { get; set; } = new();
}
public class Product
{
[Key]
public int Id { get; set; }
[Required]
public string Name { get; set; }
[Required]
public Recipe Recipe { get; set; }
}
You can instantiate and use as per below:
public static void Main()
{
var recipe = new Recipe
{
Name = "My Recipe",
Products = new List<Product>
{
new Product { Name = "Product 1" },
new Product { Name = "Product 2" },
new Product { Name = "Product 3" }
}
};
recipe.Products.ForEach(product =>
{
Console.WriteLine(product.Name);
});
}
It sounds like you are looking for a many-to-many relationship rather than a one-to-many.
If you are using Code-First and EF6 or EF Core 5+ then you can use:
public class Recipe
{
[Key]
public int Id { get; set; }
[Required]
public string Name { get; set; }
public virtual ICollection<Product> Products { get; set; } = new List<Product>();
}
public class Product
{
[Key]
public int Id { get; set; }
[Required]
public string Name { get; set; }
public virtual ICollection<Recipe> Recipes { get; set; } = new List<Recipe>();
}
To understand what is happening behind the scenes, EF should create a joining table called ProductRecipe or RecipeProduct which contains two FKs. ProductId and RecipeId. These will also form a composite PK for this table. Using this table, EF can associate one product to several recipes while also associating one recipe to the various products. In the object model you get the collection of products for each recipe, and the collection of recipes for each product.
With earlier versions of EF Core you were limited to having to declare this linking entity so you would have something more like:
public class Recipe
{
// ...
public virtual ICollection<ProductRecipe> ProductRecipes { get; set; } = new List<ProductRecipe>();
}
public class Product
{
// ...
public virtual ICollection<ProductRecipe> ProductRecipes { get; set; } = new List<ProductRecipe>();
}
public class ProductRecipe
{
[Key, Column(Order = 0)]
public int ProductId { get; set; }
[Key, Column(Order = 1)]
public int RecipeId { get; set; }
public virtual Product Product { get; set; }
public virtual Recipe Recipe { get; set; }
}
This approach is still an option in the other versions of EF, and is required if you want to support adding any additional fields to the joining table. For instance if you want to track things like CreatedDate/ModifiedDate etc. to record when a product was associated to a recipe etc. To expose that information to an application through EF, EF needs to know about the ProductRecipe as an entity. The trade off is that this approach is "messier" when usually you care about the Products for a Recipe etc. To get a list of products for a recipe you would need:
var products = context.Products
.Where(p => p.ProductRecipes.Any(pr => pr.RecipeId == recipeId)
.ToList();
or
var products = context.Recipies
.Where(r => r.RecipeId == recipeId)
.SelectMany(r => r.ProductRecipies.Select(pr => pr.Product).ToList())
.ToList();
vs. the implied joining table in the first approach:
var produts = context.Recipes
.Where(r => r.RecipeId == recipeId)
.SelectMany(r => r.Products)
.ToList();
... which is arguably easier to read.

EF Include Child Collection returning Nulls in list

I am having an issue with EF returning NULL values within a child list. Here is my model that I am trying to get:
public class CompoundIngredient : Ingredient
{
public List<MeasuredIngredient> MeasuredIngredients { get; set; }
public string UserId { get; set; }
public CompoundIngredient()
{
MeasuredIngredients = new List<MeasuredIngredient>();
IsPublic = true;
}
}
However, when I do this:
return await _dataContext.CompoundIngredients
.Include(a => a.MeasuredIngredients)
.ThenInclude(a => a.MeasurementType)
.Include(a => a.MeasuredIngredients)
.ThenInclude(a => a.Ingredient)
.ThenInclude(a => a.IngredientType)
.FirstOrDefaultAsync(c => c.DisplayValue == name);
I get back a collection of 4 items. 2 items are populated and 2 are NULL.
Here is the data in the DB
As you can see from the picture there are 4 entries in the table, 2 of which belong to CompoundIngredientId 6 which is the ID of the ingredient who matches the Name value.
Why am I getting 4 results back, 2 of which are null?
EDIT:
So here are the models
public class CompoundIngredient : Ingredient
{
public List<MeasuredIngredient> MeasuredIngredients { get; set; }
public string UserId { get; set; }
public CompoundIngredient()
{
MeasuredIngredients = new List<MeasuredIngredient>();
IsPublic = true;
}
}
public class Ingredient
{
public int Id { get; set; }
public string DisplayValue { get; set; }
public string Description { get; set; }
public bool IsPublic { get; set; }
public IngredientType IngredientType { get; set; }
public int IngredientTypeId { get; set; }
public int CompanyId { get; set; }
public string UserName { get; set; }
public string CreatedBy { get; set; }
public DateTime CreatedDate { get; set; }
public string LastModifiedBy { get; set; }
public DateTime LastModifiedDate { get; set; }
}
public class MeasuredIngredient
{
public int Id { get; set; }
public decimal Amount { get; set; }
public int MeasurementTypeId { get; set; }
public MeasurementType MeasurementType { get; set; }
public int IngredientId { get; set; }
public Ingredient Ingredient { get; set; }
public int? UseId { get; set; }
public Use Use { get; set; }
}
public class Recipe
{
public int Id { get; set; }
public string UserId { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public bool IsPublic { get; set; }
public int RecipeCategoryId { get; set; }
public RecipeCategory RecipeCategory { get; set; }
public int SocialMediaId { get; set; }
public SocialMedia SocialMedia { get; set; }
public virtual List<TimeTemp> TimeTemps { get; set; }
public virtual List<RecipeFuel> RecipeFuels{ get;set; }
public List<MeasuredIngredient> MeasuredIngredients { get; set; }
public List<RecipeStep> RecipeSteps { get; set; }
public string CreatedBy { get; set; }
public DateTime CreatedDate { get; set; }
public string LastModifiedBy { get; set; }
public DateTime LastModifiedDate { get; set; }
public Recipe()
{
MeasuredIngredients = new List<MeasuredIngredient>();
RecipeSteps = new List<RecipeStep>();
SocialMedia = new SocialMedia();
RecipeFuels = new List<RecipeFuel>();
TimeTemps = new List<TimeTemp>();
IsPublic = true;
}
}
As you can see Measured Ingredient isnt exclusive to CompoundIngredient. Recipe also has a List on it as well.
as far as configurations i dont have much
public class MeasuredIngredientConfiguration : IEntityTypeConfiguration<MeasuredIngredient>
{
public void Configure(EntityTypeBuilder<MeasuredIngredient> builder)
{
builder.Property(p => p.UseId).IsRequired(false);
}
}
public class IngredientConfiguration : IEntityTypeConfiguration<Ingredient>
{
public void Configure(EntityTypeBuilder<Ingredient> builder)
{
builder.Property(p => p.IsPublic).HasDefaultValue(true);
}
}
public class RecipeConfiguration : IEntityTypeConfiguration<Recipe>
{
public void Configure(EntityTypeBuilder<Recipe> builder)
{
builder.Property(p => p.IsPublic).HasDefaultValue(true);
}
}
here are the tables in the DB with FKs
I highly suspect the issue will stem from CompoundIngredient inheriting from Ingredient using TPH inheritance (One table with a Discriminator) then being referenced by MeasuredIngredient, and this relationship not being set up quite right. Which version of EF Core is this?
Overall the relationship between these entities/tables feels "odd". You have an ingredient, then a "compound" ingredient that is made up of one or more MeasuredIngredient. (which does not extend ingredient) A Measured ingredient contains one Ingredient, and optionally one CompoundIngredient.
Given a compound ingredient represents just a collection of measured ingredients, this just feels a bit off. It sounds like you want a recipe to contain a list of ingredients where each is associated with a measurement (The MeasuredIngredient) but that "ingredient" may be a combination of other ingredients (with associated measurements) where you may want to possibly avoid duplicating data.
I built a simple test with the core relationships in EF Core 5 and I was able to get the expected results. The important detail here was ensuring the relationship between the 3 classes (and possibly other related classes) is configured correctly. For example, cutting down the object model down to the core I came up with:
public class Ingredient
{
public int Id { get; set; }
public string Name { get; set; }
}
public class CompoundIngredient : Ingredient
{
public virtual ICollection<MeasuredIngredient> MeasuredIngredients { get; set; } = new List<MeasuredIngredient>();
}
public class MeasuredIngredient
{
public int Id { get; set; }
publi int IngredientId { get; set; }
public virtual Ingredient Ingredient { get; set; }
}
Creating test records with the relationships you showed and running EF Core 5 I wasn't able to reproduce the issue, but I honestly did not feel comfortable with leaving EF to sort out the discriminator and relationships.
The bits I didn't like were:
CompoundIngredient extends Ingredient while containing a Many relationship to MeasuredIngredient where there is no corresponding "One" relationship on MeasuredIngredient, but it does have a "One" relationship with Ingredient.
The discriminator here is implied, not configured.
What I am more comfortable with was:
public class Ingredient
{
public int Id { get; set; }
public string Name { get; set; }
}
public class CompoundIngredient : Ingredient
{
public virtual ICollection<MeasuredIngredient> MeasuredIngredients { get; set; } = new List<MeasuredIngredient>();
}
public class MeasuredIngredient
{
public int Id { get; set; }
public virtual Ingredient Ingredient { get; set; }
public virtual CompoundIngredient CompoundIngredient { get; set; }
}
Then explicitly mapping the relationships to ensure there is no confusion on FKs:
public class MeasuredIngredientConfiguration : IEntityTypeConfiguration<MeasuredIngredient>
{
public void Configure(EntityTypeBuilder<MeasuredIngredient> builder)
{
builder.Property(p => p.UseId).IsRequired(false);
builder.HasOne(p => p.Ingredient)
.WithMany()
.IsRequired()
.HasForeignKey("IngredientId");
builder.HasOne(p => p.CompoundIngredient)
.WithMany(p => p.MeasuredIngredients)
.IsRequired(false)
.HasForeignKey("CompoundIngredientId");
}
}
public class IngredientConfiguration : IEntityTypeConfiguration<Ingredient>
{
public void Configure(EntityTypeBuilder<Ingredient> builder)
{
builder.Property(p => p.IsPublic).HasDefaultValue(true);
builder.HasDiscriminator<string>("Discriminator")
.HasValue<Ingredient>("I")
.HasValue<CompoundIngredient>("C"); // Whichever discriminator values you want to use.
}
}
I generally do not have FKs exposed in entities for navigation properties, opting for shadow properties. This should work just as well with the FK fields mapped.
Now I had excluded this configuration and this example did work with EF Core 5. I was also trying to force a misconfiguration around possibly the CompoundIngredientId and IngredientId in the measured ingredient, but outside of generating specific configuration errors around missing assumed FKs I wasn't able to reproduce your issue. It could also be behaviour specific to the version of EF Core you are using.
You could try adding the explicit mapping to see if that solves or otherwise changes your results. Getting null entries in your collection smells like EF is trying to parse the CompoundIngredient -> MeasuredIngredient, but it is getting other measured Ingredients with the same Ingredient reference (1-2) but not the matching compound ingredient ID. It's definitely a weird one.
Otherwise I would look to temporarily eliminate all other references such as Recipe, measurement type, etc. down to the simplest possible example and data set that reproduces the problem. This becomes easier to investigate options to identify where/what is getting mixed up.
Hopefully this gives you some ideas on how to get to the bottom of the issue.
Turns out the issue is not with EF… after looking further into it EF is returning the proper counts and relations. The issue is higher up during the deserialization of the json being returned from the API. I created a new question for this
JsonSerializer.DeserializeAsync<> Creating Null Items in Collections

Load related data without foreign key constraint in ef-core-2.1

I want to load related entities data Parent by using Eager Loading O/RM pattern. But I can't specify a foregin key constraint on ParentId because it creates a cycle which is not allowed. Currently, I'm using an inner join to load Parent data explicitly.
Here is my Domain Model that I'm using.
[Table("Category")]
public class CategoryDM
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int CategoryId { get; set; }
public string Name { get; set; }
public string Description { get; set; }
[Display(Name="Parent")]
public int ParentId { get; set; }
[NotMapped]
public CategoryDM Parent { get; set; }
}
Is there any way to load related entities like this? or any other recommended way to achieve this.
var result = _context.Category.Include(e => e.Parent);
This should work fine, here is an exemplary working model.
Model
public class Category : ISelfRelated<Category>
{
public int Id { get; set; }
public string Name { get; set; }
public string ThumbnailUrl { get; set; }
public int? ParentId { get; set; }
public Category Parent { get; set; }
public IEnumerable<Category> Children { get; set; }
}
Model configuration
category.HasOne(c => c.Parent)
.WithMany(c => c.Children)
.HasForeignKey(c => c.ParentId)
.HasPrincipalKey(c => c.Id)
.OnDelete(DeleteBehavior.Restrict)
.IsRequired(false);

How to map recipes with ingredients using AutoMapper

I have following RecipeModel, IngredientModel and RecipePartModel classes which represent the DTO classes for the frontend user:
public class RecipeModel
{
public Guid Id { get; set; }
public string Name { get; set; }
public string ImageUrl { get; set; }
public string Description { get; set; }
public IEnumerable<RecipePartModel> RecipeParts { get; set; }
}
public class IngredientModel
{
public Guid Id { get; set; }
public string Name { get; set; }
}
public class RecipePartModel
{
public Guid Id { get; set; }
public IngredientModel Ingredient { get; set; }
public string Unit { get; set; }
public decimal Quantity { get; set; }
}
Here are my entity classes:
public class Recipe : BaseEntity
{
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
[Key]
public Guid Id { get; set; }
public string Name { get; set; }
public string ImageUrl { get; set; }
public string Description { get; set; }
public virtual IEnumerable<RecipePart> RecipeParts { get; set; }
}
public class Ingredient : BaseEntity
{
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
[Key]
public Guid Id { get; set; }
public string Name { get; set; }
public int Amount { get; set; }
public virtual IEnumerable<RecipePart> RecipeParts { get; set; }
}
public class RecipePart : BaseEntity
{
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
[Key]
public Guid Id { get; set; }
public Ingredient Ingredient { get; set; }
public Recipe Recipe { get; set; }
public string Unit { get; set; }
public decimal Quantity { get; set; }
}
My question is - how can I map the Recipe to RecipeModel using AutoMapper? I tried something like this but I assume it is bad, because it just join all the RecipeParts for the whole database, am I correct?
public class DomainProfile : Profile
{
public DomainProfile()
{
CreateMap<Ingredient, IngredientModel>().ReverseMap();
CreateMap<Recipe, RecipeModel>()
.ForMember(x => x.RecipeParts, opt => opt.MapFrom(src => src.RecipeParts));
}
}
To answer your question about how to use AutoMapper to map a type to another type, there are many ways of doing this. Documentation is here: http://docs.automapper.org/en/stable/Getting-started.html.
I wrote a console app and got it working in the quickest way I know possible using your code. When I debug this, and check inside recipeModel, it references a list of RecipePartModels with a single RecipePartModel. Inside that RecipePartModel, it references an IngredientModel.
static void Main(string[] args)
{
var profile = new DomainProfile();
Mapper.Initialize(cfg => cfg.AddProfile(profile));
var recipe = new Recipe
{
RecipeParts = new List<RecipePart>
{
new RecipePart()
{
Ingredient = new Ingredient()
}
}
};
var recipeModel = Mapper.Map<Recipe, RecipeModel>(recipe);
Console.ReadKey();
}
To answer your concern about getting all recipes from the database, if you're using Entity Framework, it depends on if you have lazy loading turned on. Lazy loading ensures that, when you get a recipe from the database, the recipe parts will not be loaded. They will only be loaded when you access the recipe part directly later on in the program flow. Lazy loading is turned on by default so this is the default behaviour. If you turn it off, you've enabled eager loading which loads all recipe parts and in turn their ingredient.
This might help: http://www.entityframeworktutorial.net/lazyloading-in-entity-framework.aspx.
There is nothing bad about this mapping. In fact you don't even need the ForMember call as this is the default convention. The mapping will simply convert each element in the entity child collection to a corresponding model object.
Of course, whether you load your entities in an efficient manner is another matter. If you load a large amount of Recipe entities, and lazy load the RecipeParts collections for each, you will have a major "SELECT N+1" problem. But this is not the fault of AutoMapper.

EF Code First Fluent Mapping: 0-1 to Many: HasOptional(), same table

I have a "Category" Entity as follow:
public class Category
{
//<Summary>
//Fields...
//</Summary>
public Guid CategoryId { get; set; }
public string CategoryName { get; set; }
public bool IsDelete { get; set; }
// Fields for relationships
public Guid MainCategoryId { get; set; }
public Category MainCategory { get; set; }
public virtual ICollection<Category> ChildCategories { get; set; }
}
As seen above I want to create 0-one-to-many relationship in same table. I used Fluent API for this as follows:
HasRequired(category => category.MainCategory)
.WithMany(category => category.ChildCategories)
.HasForeignKey(category => category.MainCategoryId);
But it is a one-to-many, isn't 0-1-to-many. I use HasOptional, but it give me an error.
How can I do this with Fluent API?
thanks for reply
Make the MainCategoryId property nullable:
public Guid? MainCategoryId { get; set; }
And then you can use HasOptional method:
HasOptional(category => category.MainCategory)
.WithMany(category => category.ChildCategories)
.HasForeignKey(category => category.MainCategoryId);

Categories

Resources