AutoMapper / AutoMapper.Extensions.ExpressionMapping

MIT License
143 stars 39 forks source link

Filter on unsigned type for trivially-mapped relation yields InvalidOperationException #176

Closed bhood-zorus closed 10 months ago

bhood-zorus commented 10 months ago

Original issue: https://github.com/AutoMapper/AutoMapper.Extensions.OData/issues/204

Source/destination types

// Source: EF entity
public class TestEntity
{
    public int Id { get; set; }
    public ulong UserId { get; set; }
}

// Destination: API model
public class TestModel
{
    public int? Id { get; set; }
    public ulong? UserId { get; set; }
}

Mapping configuration

public class TestModelProfile : Profile
{
    public TestModelProfile()
    {
        CreateMap<TestEntity, TestModel>();
    }
}

Version: 4.0.1

.NET 6 AutoMapper.AspNetCore.OData.EFCore 4.0.1 AutoMapper 12.0.1 AutoMapper.Collection 9.0.0 AutoMapper.Extensions.ExpressionMapping 6.0.4 AutoMapper.Extensions.DependencyInjection 12.0.1

Expected behavior

Results are returned normally

Actual behavior

System.InvalidOperationException: For members of literal types, use IMappingExpression.ForMember() to make the parent property types an exact match. Parent Source Type: System.Nullable`1[System.UInt64], Parent Destination Type: System.UInt64, Full Member Name "Value".

Steps to reproduce

Expression mapping

Expression<Func<TestModel, bool>> expression = src => src.UserId != 1;
Expression<Func<TestEntity, bool>> mappedExpression = mapper.MapExpression<Expression<Func<TestEntity, bool>>>(expression);

Using OData

Sample project: https://github.com/bhood-zorus/AutoMapperBugDemo Send a GET request to https://localhost:port/api/test?$filter=UserId eq 1

When the UserId property of TestEntity and TestModel is of type int and int? (respectively), the API request succeeds. When it's of an unsigned type (uint, ulong, etc.) the application throws the exception above.

The sample project uses an in-memory database for the sake of providing a simple repro. This behavior is currently being seen with a real-world MySQL database using the Pomelo EF provider.

leandrosalgado commented 10 months ago

I have a similar issue with DateTime and DateTimeOffset:

System.InvalidOperationException: For members of literal types, use IMappingExpression.ForMember() to make the parent property types an exact match. Parent Source Type: System.Nullable1[System.DateTimeOffset], Parent Destination Type: System.Nullable1[System.DateTime], Full Member Name \"Value.Year\"

leandrosalgado commented 10 months ago

The way I was able to workaround this issue even though I have a map defined for DateTimeOffset? <=> DateTime? was to add a .ForMember for each DateTimeOffset? property that mapped to a entity property with DateTime? (all DB has all date columns column set as datetime and we only use UTC) forcing the cast of DateTime? to DateTimeOffset? . It seemed redundant but fixed the issue for me. Probably it may fix your issue also if you create maps for int, int?, uint, ulong, etc.

So I had to add all these maps. I'm not sure if I really need them but it was the only way to make the OData filters work properly when filtering expanded properties for me:

  CreateMap<DateTime, DateTimeOffset>().ConvertUsing(src => (DateTimeOffset)src);
  CreateMap<DateTime?, DateTimeOffset?>().ConvertUsing(src => (DateTimeOffset?)src.Value);
  CreateMap<DateTimeOffset, DateTime>().ConvertUsing(src => src.UtcDateTime);
  CreateMap<DateTimeOffset?, DateTime?>().ConvertUsing(src => (DateTime?)src.Value.UtcDateTime);
  CreateMap<DateTimeOffset?, DateTimeOffset>().ReverseMap();
  CreateMap<DateTimeOffset?, Microsoft.OData.Edm.Date?>().ReverseMap();

On top of that I had to add this cast for each DateTimeOffset otherwise filtering using only date (dateField eq 2024-01-24) would fail with the error above.

.ForMember(dest => dest.DateField, opt => opt.MapFrom(src => (DateTimeOffset?)src.DateField.Value))
BlaiseD commented 10 months ago

You'll want to add the failing expression mapping so anyone trying to help can run it an see it fail e.g.

            Expression<Func<TestModel, bool>> expression = src => src.UserId != 1;
            Expression<Func<TestEntity, bool>> mappedExpression = mapper.MapExpression<Expression<Func<TestEntity, bool>>>(expression);
bhood-zorus commented 10 months ago

Thanks, I've put that into the message above the original reproduction steps.

BlaiseD commented 10 months ago

This doesn't throw:

[Fact]
public void CanMap()
{
    var mapper = new AutoMapper.MapperConfiguration(cfg =>
    {
        cfg.CreateMap<TestEntity, TestModel>();

    }).CreateMapper();

    Expression<Func<TestModel, bool>> expression = src => src.UserId != 1;
    Expression<Func<TestEntity, bool>> mappedExpression = mapper.MapExpression<Expression<Func<TestEntity, bool>>>(expression);
}

// Source: EF entity
public class TestEntity
{
    public int Id { get; set; }
    public ulong UserId { get; set; }
}

// Destination: API model
public class TestModel
{
    public int? Id { get; set; }
    public ulong? UserId { get; set; }
}

You can still post the failing expression here or open a new issue.

bhood-zorus commented 10 months ago

@BlaiseD You gave me the expression, so I reasonably assumed it would be relevant to the issue. You were also the one who advised that I create the issue in this repository. It seems that I'm running out of options and have to learn the inner workings of this library, at which point I submit the PR with the fix myself.

There is (and always has been) an api project in the steps to reproduce, and it will readily reproduce the problem.