AutoMapper/AutoMapper.Extensions.ExpressionMapping

Entity Enum Property with String Conversion is Cast to int

Closed this issue · 5 comments

some sample code for context:

namespace Data
{
    public enum OrganizationType
    {
        Undefined,
        System,
        Firm,
        Client
    }

    public class Organization
    {
        public Guid Id { get; set; }
        public string Name { get; set; }
        public Data.OrganizationType Type { get; set; }
    }

    public class OrganizationTypeConfiguration : IEntityTypeConfiguration<Organization>
    {
        protected override void Configure(EntityTypeBuilder<Organization> builder)
        {
            builder.Property(x => x.Name)
                .IsRequired();

            builder
                .Property(x => x.Type)
                .HasConversion<string>();
        }
    }

}

namespace Model
{
    public enum OrganizationType
    {
        Undefined,
        System,
        Firm,
        Client
    }
    
    public class Organization
    {
        public Guid Id { get; set; }
        public string Name { get; set; }
        public Model.OrganizationType Type { get; set; }
    }
}

CreateMap<Data.Organization, Model.Organization>()
    .ForMember(dest => dest.Id, opts => opts.MapFrom(src => src.Id))
    .ForMember(dest => dest.Name, opts => opts.MapFrom(src => src.Name))
    .ForMember(dest => dest.Type, opts => opts.MapFrom(src => src.Type));
                
[HttpGet]
[ODataRoute("Organizations")]
[ProducesResponseType(typeof(ODataValue<IEnumerable<Model.Organization>>), Status200OK)]
public async Task<IActionResult> Get(ODataQueryOptions<Model.Organization> options) =>
    Ok(await DbContext.Organizations.GetQueryAsync(Mapper, options, null));

If I use the DbContext directly and then AutoMapper to map my entity to my dto everything works great. When I query for Organizations with OData expression mapping, I get a sql client exception due to an invalid cast. This is due to the following sql being generated for this query:

SELECT [o].[Id], [o].[Name], CAST([o].[Type] AS int)...

The Type column is nvarchar (see above, enum is converted to string) and so this throws an exception

...Conversion failed when converting the nvarchar value 'System' to data type int...

whatever is generating this query seems to be assuming the Type enum is stored as an int when, in fact, I have a conversion in place to store eums as strings. My assumption is that the expression mapping may be using the enum ordinal value instead of the enum value, preventing EF from handling this conversion. Is there a way to work around this or some configuration I am missing somewhere?

Things I have tried:

  • an explicit mapping between then enums with my own mapping function
  • an explicit mapping between the enums using .ConvertUsingEnumMapping()
    • tried default, .MapByName(), .MapByValue(), and explicit .MapValue()
  • IValueConverter with .ForMember(dest => dest.Type, opts => opts.ConvertUsing<OrganizationTypeValueConverter, Data.OrganizationType>())
  • ITypeConverter with CreateMap<Data.OrganizationType, Model.OrganizationType>().ConvertUsing<OrganizationTypeEntityToModelConverter>();
    • including this type converter actually results in a different error Argument types do not match thrown from AutoMapper.QueryableExtensions.Impl.MappedTypeExpressionBinder.BindMappedTypeExpression
  • ITypeConverter with CreateMap<Model.OrganizationType, Data.OrganizationType>().ConvertUsing<OrganizationTypeModelToEntityConverter>();

Thanks!

This issue log is for questions about the expression it itself.

You'll want to show the expected expression and what's being generated based on the configuration .ForMember(dest => dest.Type, opts => opts.MapFrom(src => src.Type));.

If that is incorrect then maybe we can help or create a bug.

@BlaiseD I pulled this from my logs. If this isn't what you're looking for, please let me know.

DbSet<Organization>()
    .Select(dtoOrganization => new Organization{ 
        Enabled = dtoOrganization.Enabled, 
        Id = dtoOrganization.Id, 
        Name = dtoOrganization.Name, 
        Type = (ModelOrganizationType)dtoOrganization.Type, 
        Users = dtoOrganization.Users
            .Select(dtoUser => new User{ 
                Enabled = dtoUser.Enabled, 
                Id = dtoUser.Id, 
                IdentityId = dtoUser.IdentityId, 
                OrganizationId = dtoUser.OrganizationId, 
                Roles = dtoUser.UserRoles
                    .Select(x => x.Role.Name) 
            }
            )
            .ToArray() 
    }
    )

The expression has some additional properties that I excluded from my code sample above. I also renamed my OrganizationType enums as ModelOrganizationType and DataOrganizationType so it'd be clear which type was being used in the cast.

For expected behavior, I wouldn't expect the cast in Type = (ModelOrganizationType)dtoOrganization.Type, . At any rate, the cast is incorrect as dtoOrganization.Type is a DataOrganizationType.

If I have to, I can work around this by having the entity and model share the enum, but I would not prefer this. Is this a bug or is there a possibility I just have some kind of enum mapping incorrect?

if I include an enum type mapping such as

 CreateMap<Data.DataOrganizationType, Security.Model.V1.ModelOrganizationType>().ConvertUsingEnumMapping(opts => opts.MapByName());

this results in the following expression:

DbSet<Organization>()
    .Select(dtoOrganization => new Organization{ 
        Enabled = dtoOrganization.Enabled, 
        Id = dtoOrganization.Id, 
        Name = dtoOrganization.Name, 
        Type = CustomMapExpressionFactory<DataOrganizationType, ModelOrganizationType>.ConvertEnumValue(dtoOrganization.Type), 
        Users = dtoOrganization.Users
            .Select(dtoUser => new User{ 
                Enabled = dtoUser.Enabled, 
                Id = dtoUser.Id, 
                IdentityId = dtoUser.IdentityId, 
                OrganizationId = dtoUser.OrganizationId, 
                Roles = dtoUser.UserRoles
                    .Select(x => x.Role.Name) 
            }
            )
            .ToArray() 
    }
    )

and error:

System.InvalidOperationException: The client projection contains a reference to a constant expression of 'AutoMapper.Extensions.EnumMapping.Internal.CustomMapExpressionFactory<LeaseCrunch.Security.Data.DataOrganizationType, LeaseCrunch.Security.Model.V1.ModelOrganizationType>' through the instance method 'ConvertEnumValue'. This could potentially cause a memory leak; consider making the method static so that it does not capture constant in the instance.

Take this repo.

And add a test like the following:

        [Fact]
        public void MapExpression()
        {
            Expression<Func<EnrollmentModel, Contoso.Domain.Entities.Grade>> expression = e => e.Grade1;
            IMapper mapper = serviceProvider.GetRequiredService<IMapper>();
            Expression<Func<Enrollment, Contoso.Data.Entities.Grade>> mapped = mapper.Map<Expression<Func<Enrollment, Contoso.Data.Entities.Grade>>>(expression);
        }

I've added public Grade Grade1 { get; set; } to both Enrollment and EnrollmentModel in that repository. Start with a sample like that for your code and establish whether or not the member expression is what you expect.

Result:

".Lambda #Lambda1<System.Func`2[Contoso.Data.Entities.Enrollment,Contoso.Data.Entities.Grade]>(Contoso.Data.Entities.Enrollment $e)\r\n{\r\n    $e.Grade1\r\n}"

image

You'll want to determine whether or not you expression is being mapped as expected and go from there. Pull down the code if you have to.

if I simplify things down to:

var sc = new ServiceCollection();
sc.AddAutoMapper(typeof(Startup).Assembly);
var sp = sc.BuildServiceProvider();
var mapper = sp.GetRequiredService<IMapper>();
Expression<Func<ModelOrganization, ModelOrganizationType>> exp = x => x.Type;
var result = mapper.Map<Expression<Func<Organization, OrganizationType>>>(exp);

the result is the correct expression. Should I take these questions to AutoMapper.Extensions.OData?