Self referencing loop causing stack overflow with map - c#

According to the documentation PreserveReferences should be set automatically whenever possible for AutoMapper.
Starting from 6.1.0 PreserveReferences is set automatically at config
time whenever possible.
https://github.com/AutoMapper/AutoMapper/wiki/5.0-Upgrade-Guide
I have also tried setting MaxDepth to 1 but I still get a stack overflow exception with the following mapping. Can I get around this somehow or do I need to modify the view models?
cfg.CreateMap<ArticleViewModel, Article>(MemberList.Source)
.MaxDepth(1)
.EqualityComparison((src, dst) => src.Id == dst.Id);
Code that causes the stack overflow exception:
var article = await iArticleRepository.GetAsync(id);
//The mapping below causes the exception
var mappedArticle = Mapper.Map<ArticleViewModel>(article);
Entities:
public class Article: IEntity<int>
{
[Key]
public int Id { get; set; }
...
public int SupplierId { get; set; }
public virtual Supplier Supplier { get; set; }
}
public class Supplier: IEntity<int>
{
[Key]
public int Id { get; set; }
...
public virtual ICollection<Contact> Contacts { get; set; }
}
public class Contact: IEntity<int>
{
[Key]
public int Id { get; set; }
...
public virtual ICollection<Supplier> Suppliers { get; set; }
}
View models:
public class ArticleViewModel
{
public int Id { get; set; }
...
public SupplierViewModel Supplier { get; set; }
}
public class SupplierViewModel
{
public int Id { get; set; }
...
public List<ContactViewModel> Contacts { get; set; }
}
public class ContactViewModel
{
public int Id { get; set; }
...
public List<SupplierViewModel> Suppliers { get; set; }
}

Well, it's unclear what does whenever possible mean. Since the documentation before that states
It turns out this tracking is very expensive, and you need to opt-in using PreserveReferences for circular maps to work
looks like your scenario fails into not possible category :)
Let not rely on that and use the explicit opt-in. The circular reference in this sample model is between Supplier and Contact, so you have to specify in one of the involved class mappings, for instance:
cfg.CreateMap<ArticleViewModel, Article>(MemberList.Source)
.MaxDepth(1)
.EqualityComparison((src, dst) => src.Id == dst.Id);
cfg.CreateMap<SupplierViewModel, Supplier>(MemberList.Source)
.PreserveReferences()
.EqualityComparison((src, dst) => src.Id == dst.Id);

Related

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

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.

Loading related entities in EF6

I have 3 tables which are related to each others:
Product has many SuggestedPrices
Product has many ProductPricing
so i want to retrieve Suggested Prices like so :
await ctx.SuggestedPrices
.OrderByDescending(pp => pp.SuggestionDate)
.Include(p1 => p1.Customer)
.Include(p2 => p2.Product)
.Include(p3 => p3.Product.ProductPricing)
.ToListAsync()
According to your request: i added the followings:
Product class :
public class Product : ReportingBase {
// Product-ProductPricing -> One Product has many Prices
public virtual ICollection<ProductPricing> ProductPricing { get; set; }
// Product-SuggestedPrices
public virtual ICollection<SuggestedPrice> SuggestedPrices { get; set; }
}
ProductPricing class :
public class ProductPricing {
// ProductPricings-Product
public virtual Product Product { get; set; }
public int ProductId { get; set; }
}
SuggestedPrice class:
public class SuggestedPrice : EntityBase {
// SuggestedPrices-Product
public virtual Product Product { get; set; }
public int ProductId { get; set; }
// SuggestedPrices-Customer
public virtual ApplicationUser Customer { get; set; }
public string CustomerId { get; set; }
}
but when i add line .Include(p3 => p3.Product.ProductPricing) i get an error which says:
Use dotted paths for reference navigation properties and the Select operator for collection navigation properties
how do i get rid of this error?
thank to your answers
After some struggling I found out that there's no need to include Product.ProductPricing and it's already there:
OrgPrice = p.Product.ProductPricing.FirstOrDefault().OrgPrice,
PriceAfterDiscount = p.Product.ProductPricing.FirstOrDefault().Price

C# Entity Framework 6 - AutoMapper 6 - Collections/List - Add and Update works but removing item throws an error

As the title says I can add and update but when it comes to deleting I get an error.
The operation failed: The relationship could not be changed because
one or more of the foreign-key properties is non-nullable. When a
change is made to a relationship, the related foreign-key property is
set to a null value. If the foreign-key does not support null values,
a new relationship must be defined, the foreign-key property must be
assigned another non-null value, or the unrelated object must be
deleted.
I understand that the Description in this case only gets a null foreign key but is never deleted. I have seen some examples where they suggest looping through every child item and delete them one by one. Imao I think there should be a better way. What I'm looking for is a solution with minimal impact and just tell EF to delete the entire item and not only null the foreign key.
https://stackoverflow.com/a/5540956/3850405
Using AutoMapper, AutoMapper.Collection and AutoMapper.Collection.EntityFramework.
Controller method:
public async Task<IHttpActionResult> UpdateArticle(ArticleViewModel articleVm)
{
Article articleOriginal = await iArticleRepository.GetAsync(articleVm.Id);
Article updatedArticle = Mapper.Map<ArticleViewModel, Article>(articleVm, articleOriginal);
await iArticleRepository.UpdateAsync(updatedArticle);
return Ok();
}
Mapping:
Mapper.Initialize(cfg =>
{
cfg.AddCollectionMappers();
cfg.SetGeneratePropertyMaps<GenerateEntityFrameworkPrimaryKeyPropertyMaps<DbContext>>();
cfg.CreateMap<ArticleViewModel, Article>(MemberList.Source)
.EqualityComparison((src, dst) => src.Id == dst.Id);
cfg.CreateMap<DescriptionViewModel, Description>(MemberList.Source)
.EqualityComparison((src, dst) => src.Id == dst.Id);
}
Mapper.AssertConfigurationIsValid();
Viewmodels:
public class ArticleViewModel
{
public int Id { get; set; }
public string Name { get; set; }
public List<DescriptionViewModel> Descriptions { get; set; }
}
public class DescriptionViewModel
{
public int Id { get; set; }
public string Heading { get; set; }
}
Models:
public class Article : IEntity<int>
{
public Article()
{
Descriptions = new List<Description>();
}
[Key]
public int Id { get; set; }
public DateTime Created { get; set; }
public DateTime Updated { get; set; }
[MaxLength(256)]
public string Name { get; set; }
public virtual ICollection<Description> Descriptions { get; set; }
}
public class Description: IEntity<int>
{
[Key]
public int Id { get; set; }
public DateTime Created { get; set; }
public DateTime Updated { get; set; }
[MaxLength(256)]
public string Heading { get; set; }
public int ArticleId { get; set; }
public virtual Article Article { get; set; }
}
Got a solution from this answer
https://stackoverflow.com/a/32983252/3850405
and this blog:
http://www.kianryan.co.uk/2013/03/orphaned-child/
Code:
public class Description: IEntity<int>
{
[Key, Column(Order = 0), DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
public DateTime Created { get; set; }
public DateTime Updated { get; set; }
[MaxLength(256)]
public string Heading { get; set; }
[Key, Column(Order = 1)]
public int ArticleId { get; set; }
public virtual Article Article { get; set; }
}
I can really recommend reading Mosh's answer about the difference between composition and aggregation since it will help you understand EF better.

Unable to Determine Principal End - Entity Framework Code First Relationship

PLEASE CHECK POSSIBLE SOLUTION SECTION BELOW
I have an issue with a foreign key relationship - here are my tables:
public class Lead
{
[Key]
public int LeadId { get; set; }
public int? CustomerId { get; set; }
[ForeignKey("CustomerId")]
public virtual Customer Customer { get; set; }
}
public class Customer
{
[Key]
[Column("customer_id")]
public int CustomerId { get; set; }
[ForeignKey("CustomerId")]
public virtual Lead Lead { get; set; }
}
I'm having an issue where I receive this error:
Unable to determine the principal end of an association between the types 'Sales.Customers.Customer' and 'Sales.Leads.Lead'. The principal end of this association must be explicitly configured using either the relationship fluent API or data annotations.
I've tried adding the relationship to the modelbuilder but it appears that its not working properly. When I do get the error message to go away its actually using the Lead.LeadId -> Customer.CustomerId as the relationship instead of Lead.CustomerId -> Customer.CustomerId relationship.
I've checked similar questions on Stackoverflow but they don't seem to match my DB structure and when I try to implement their suggestions the relationship still doesn't work properly.
Its really weird - would greatly appreciate help on this!
UPDATE
So in my attempt to get this relationship to work I've switched the keys around in the following way:
public class Lead
{
[Key]
public int LeadId { get; set; }
[ForeignKey("LeadId")]
public virtual Customer Customer { get; set; }
}
public class Customer
{
[Key]
[Column("customer_id")]
public int CustomerId { get; set; }
public int? LeadId { get; set; }
[ForeignKey("LeadId")]
public virtual Lead Lead { get; set; }
}
However, same error, still no luck - I'm really at a loss why this relationship won't work. To me it seems pretty straight forward.
UPDATE 2
Ok - after a TON of wasted time messing with this I've tried a slightly different approach:
Here are my new classes....
public class Lead
{
[Key, ForeignKey("Customer")]
public int LeadId { get; set; }
public virtual Customer Customer { get; set; }
}
public class Customer
{
[Key]
[Column("customer_id")]
public int CustomerId { get; set; }
public int? LeadId { get; set; }
public virtual Lead Lead { get; set; }
}
No more error messages with the code above! The only problem is that the relationship entity framework is creating is between the Customer.CustomerId and Lead.LeadId instead of the Customer.LeadId and Lead.LeadId - I feel like i'm SO CLOSE!
POSSIBLE SOLUTION
Ok - so after some more research I came across this post here:
EF Code First - 1-to-1 Optional Relationship
I modified my classes to this:
public class Customer
{
[Key]
[Column("customer_id")]
public int CustomerId { get; set; }
public virtual Lead Lead { get; set; }
}
public class Lead
{
[Key]
public int LeadId { get; set; }
public virtual Customer Customer { get; set; }
}
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<Customer>().HasOptional<Lead>(l => l.Lead).WithOptionalDependent(c => c.Customer).Map(p => p.MapKey("LeadId"));
base.OnModelCreating(modelBuilder);
}
Everything works GREAT! But one BIG problem...
I had to remove the LeadId property from the Customer Table.... so now I'm not sure how I can assign a LeadId when creating a new Customer (when appropriate) if there is no LeadId property to assign to?
Posting this in fluent API, it should work.
public class Lead
{
[Key]
public int LeadId { get; set; }
public virtual Customer Customer { get; set; }
}
public class Customer
{
[Key]
[Column("customer_id")]
public int CustomerId { get; set; }
public virtual Lead Lead { get; set; }
}
builder.Entity<Lead>()
.HasOptional(l => l.Customer)
.WithOptionalPrincipal()
.Map(k => k.MapKey("LeadId"));
builder.Entity<Customer>()
.HasOptional(c => c.Lead)
.WithOptionalPrincipal()
.Map(k => k.MapKey("CustomerId"));
EDIT

Categories

Resources