Search code examples
c#entity-frameworkautomapper.net-5webapi

AutoMapper map array of objects in response


Case: User can add meal and it's name, description, image and ingredients. User sends such payload

{
  "ingredients":[
    {   
        "id":1,
        "name":"Gooseberry",
        "kcal":41,
        "proteins":0.8,
        "fat":0.2,
        "carbohydrates":11.8,
        "type":null,
        "quantity":100
    }
  ],
  "title": "title",
  "image": null,
  "description":"\"<p>sdf</p>\"",
  "type":"breakfast"
}

Webapi consumes payload and for Meal saves data in DB context and takes ingredients list and go through each element in foreach and adds mealId unique number so there is reference ingredients to specific meal.

Meal entity:

    public class Meal
    {
      public int Id { get; set; }
      public string Uuid { get; set; }
      public string Title { get; set; }
      public string Description { get; set; }
      public string Image { get; set; }
      public string Author { get; set; }
      public DateTime? Created { get; set; }
      public string Type { get; set; }
      public virtual List<MealIngredientsList> Ingredients { get; set; }
    }

MealIngredientsList entity:

    public class MealIngredientsList
    {
      public int Id { get; set; }
      public double Carbohydrates { get; set; }
      public double Fat { get; set; }
      public double Kcal { get; set; }
      public string Name { get; set; }
      public double Proteins { get; set; }
      public double Quantity { get; set; }
      public string Type { get; set; }
      public string MealId { get; set; }
      [ForeignKey("Uuid")]
      public virtual Meal Meal { get; set; }
    }

MealDto data transfer object:

    public class MealDto
    {
      public string Uuid { get; set; }
      public string Title { get; set; }
      public string Description { get; set; }
      public string Image { get; set; }
      public string Author { get; set; }
      public DateTime? Created { get; set; }
      public string Type { get; set; }
      public List<MealIngredientsListDto> Ingredients { get; set; }
    }

and MealIngredientsListDto:

    public class MealIngredientsListDto
    {
      public double Carbohydrates { get; set; }
      public double Fat { get; set; }
      public double Kcal { get; set; }
      public string Name { get; set; }
      public double Proteins { get; set; }
      public double Quantity { get; set; }
      public string Type { get; set; }
      public string MealId { get; set; }
    }

So Like I said meal and ingredients are saved correctly in db. Each ingredient has a reference MealId to Meal but I am trying to map so that the response for meal contains ingredients array with ingredients that are assigned to specific meal.

Right now response looks like this and ingredients is an empty array:

{
 "status": 200,
 "list": {
    "uuid": "e7206453",
    "title": "\"<p>sdf</p>\"",
    "description": "\"<p>sdf</p>\"",
    "image": null,
    "author": "982eb82e",
    "created": "2023-04-03T23:19:20.8039742+02:00",
    "type": "breakfast",
    "ingredients": []
  }
}

I managed to map user settings so this is not an array ob objects but object with properties and I did that with

            CreateMap<UserProfileDto, UserSettingDto>()
            .ForMember(m => m.newsletterConsent, m => m.MapFrom(s => s.Settings))
            .ForMember(m => m.messageEmailNotification, m => m.MapFrom(s => s.Settings));

and using .Include(u => u.Settings) based on it's DTOs and it works fine but I have no clue how to map this to get the ingredients into an array. Tried with _context.Meals.Where(x => x.Uuid == newUuid).Include(x => x.Ingredients).OrderByDescending(x => x.Created).ToList(); but cannot manage to use mapping profile to achieve that.

Does anyone have a solution/hint for that?


Solution

  • Working solution (without mapping to DTO):

      public class Meal
      {
        [Key]
        [DatabaseGenerated(DatabaseGeneratedOption.Identity)] <-- mapping by MealId will cause not incrementing Id - use this
        public int Id { get; set; }
        public string MealId { get; set; }
        public string Title { get; set; }
        [NotMapped]
        public IEnumerable<MealIngredientsList> IngredientsList { get; set; }
        public string Description { get; set; }
        public string Image { get; set; }
        public string Author { get; set; }
        public DateTime? Created { get; set; }
        public string Type { get; set; }
        public virtual ICollection<MealIngredientsList> Ingredients { get; set; 
      }
    }
    
     public class MealIngredientsList
     {
        [Key]
        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
        public int Id { get; set; }
        public double Carbohydrates { get; set; }
        public double Fat { get; set; }
        public double Kcal { get; set; }
        public string Name { get; set; }
        public double Proteins { get; set; }
        public double Quantity { get; set; }
        public string Type { get; set; }
        [ForeignKey("Meal")] <- created foreign key MealId and model builder change default key for reference (by default is Id)
        public string MealId { get; }
        [JsonIgnore]  <-- to avoid infinite loop 'cause This error is caused by the circular reference between the Meal and MealIngredientsList entities. When you serialize the Meal entity to JSON, it also includes the related MealIngredientsList entities, which in turn reference the parent Meal entity, causing an infinite loop.
        public virtual Meal Meal { get; set; }
     }
    

    DB context to make reference by specific key(not by default by Id)

                modelBuilder.Entity<Meal>()
                .HasKey(m => m.MealId);
            modelBuilder.Entity<MealIngredientsList>()
                .HasKey(mil => new { mil.Id, mil.MealId });
    
            modelBuilder.Entity<MealIngredientsList>()
                .HasOne(mil => mil.Meal)
                .WithMany(m => m.Ingredients)
                .HasForeignKey(mil => mil.MealId);
    

    Service:

            var newMeal = new Meal
            {
                MealId = newUuid,
                Title = dto.Title,
                Description = dto.Description,
                Image = dto.Image,
                Author = user,
                Created = DateTime.Now.ToLocalTime(),
                Type = dto.Type,
                Ingredients = new List<MealIngredientsList>(), <- include reference to ingredients
            };
    
            foreach(var item in dto.IngredientsList)
            {
                var ingredients = new MealIngredientsList
                {
                    Name = item.Name,
                    Carbohydrates = item.Carbohydrates,
                    Kcal = item.Kcal,
                    Proteins = item.Proteins,
                    Quantity = item.Quantity,
                    Type = item.Type,
                    Meal = newMeal, <-- assign reference for created Meal entity
                };
                newMeal.Ingredients.Add(ingredients);
            }
    

    and at the end take all meals with its' ingredients

    _context.Meals.Include(m => m.Ingredients).ToList();
    

    and it works.