AutoMapper / AutoMapper.Extensions.ExpressionMapping

MIT License
143 stars 39 forks source link

Mapping Many-To-Many relationship maps to incorrect type/property #164

Closed ErikGjers closed 1 year ago

ErikGjers commented 1 year ago

Hi again,

The test below triggers an exception. It is a many-to-many relationship with a single of those mapped to a property of a dto, since it has a set boolean, CategoryIsPrimary. One of the expressions generated gets translated to Work instead of Category and accessing the CategoryId causes the exception. The Exception is thrown in PrependParentMemberExpression() and I am not 100% sure where the expression is translated(perhaps incorrectly), but I figured some feedback would be good here before I investigate further.

Is there something I am doing incorrectly with the setup for this?

This is the exception: System.ArgumentException: 'Property 'Int32 CategoryId' is not defined for type 'AutoMapper.Extensions.ExpressionMapping.UnitTests.ExpressionTest+Work' (Parameter 'property')'

using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using Xunit;
#nullable enable
namespace AutoMapper.Extensions.ExpressionMapping.UnitTests
{
    public class ExpressionMappingManyToMany
    {
        public class WorkDto
        {
            public WorkDto()
            {
                CategoriesId = new HashSet<int>();
            }

            public ICollection<int> CategoriesId { get; set; }
            public CategoryDto? PrimaryCategory { get; set; }
            public ICollection<CategoryDto> Categories { get; set; }
        }

        public class Work
        {
            public Work()
            {
                Categories = new HashSet<WorkCategoryRelationship>();
            }

            public int Id { get; set; }

            public virtual ICollection<WorkCategoryRelationship> Categories { get; set; }
        }

        public class CategoryDto
        {
            public int Id { get; set; }
            public string? Name { get; set; }
        }

        public class Category
        {
            public Category()
            {
                Works = new HashSet<WorkCategoryRelationship>();
            }

            public int CategoryId { get; set; }

            public virtual ICollection<WorkCategoryRelationship> Works { get; set; }
        }

        public partial class WorkCategoryRelationship
        {
            public int CategoryId { get; set; }
            public int WorkId { get; set; }
            public bool? CategoryIsPrimary { get; set; }

            public virtual Category Category { get; set; }
            public virtual Work Work { get; set; }
        }

        [Fact]
        public void Test_TranslateExpression_WithAutoMapper()
        {
            Expression<Func<WorkDto, bool>> exp = x => x.PrimaryCategory.Id == 1;
            var config = new MapperConfiguration(cfg =>
            {
                cfg.AddExpressionMapping();

                cfg.CreateMap<Work, WorkDto>()
                    .ForMember(
                        dest => dest.PrimaryCategory,
                        config =>
                            config.MapFrom(
                                src =>
                                    src.Categories.Any()
                                        ? src.Categories.Where(e => e.CategoryIsPrimary ?? false).Select(e => e.Category).Single()
                                        : null
                            )
                    )
                    .ReverseMap();
                cfg.CreateMap<Category, CategoryDto>()
                    .ForMember(dest => dest.Id, config => config.MapFrom(src => src.CategoryId))
                    .ReverseMap();
                cfg.CreateMap<int, Category>().ForMember(dest => dest.CategoryId, opt => opt.MapFrom(src => src));
            });
            var mapper = config.CreateMapper();

            var translatedExp = mapper.MapExpression<Expression<Func<WorkDto, bool>>, Expression<Func<Work, bool>>>(exp);
        }
    }
}
#nullable disable

Any assistance is greatly appreciated, much like this library. PS: I will get on the other PR in the next couple of days, if not at the start of next week.

BlaiseD commented 1 year ago

Don't recommend mapping literals like int with expression mapping. You probably want the following ForPath for "deflattening"

                cfg.CreateMap<Work, WorkDto>()
                    .ForMember(
                        dest => dest.PrimaryCategory,
                        config =>
                            config.MapFrom(
                                src =>
                                    src.Categories.Any()
                                        ? src.Categories.Where(e => e.CategoryIsPrimary ?? false).Select(e => e.Category).Single()
                                        : null
                            )
                    )
                    .ForPath(dest => dest.PrimaryCategory.Id, config => config.MapFrom(src => src.Id))
                    .ReverseMap();

Also see the configuration for this test for configuration without ForPath.