Filter on unsigned type for trivially-mapped relation yields InvalidOperationException
bhood-zorus opened this issue · 6 comments
Original issue: AutoMapper/AutoMapper.Extensions.OData#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.
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.Nullable
1[System.DateTime], Full Member Name "Value.Year"
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))
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);
Thanks, I've put that into the message above the original reproduction steps.
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.
@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.