在EFCore中使用id类到string的automapper表达式映射会导致翻译错误



我使用AutoMapper在业务逻辑对象(Blo)和数据传输对象(Dto)之间进行转换。bloc类包含一个类id,而dto类包含该id的字符串。要从数据库中加载对象,需要创建bloo级的表达式,并通过AutoMapper将其转换为dto级。

类是:

public class Blo
{
public Blo(BloId id)
{
this.Id = id;
}
public BloId Id { get; set; }
}
[Table("dtos")]
public class Dto
{
[Column("id")]
[Key]
public string Id { get; set; }
}
public class BloId
{
private readonly string _value;
public BloId(string value = null)
{
this._value = value ?? Guid.NewGuid().ToString();
}
public static bool operator ==(BloId left, BloId right)
{
if (object.ReferenceEquals(left, right))
{
return true;
}
if (left is null || right is null)
{
return false;
}
return left._value == right._value;
}
public static bool operator !=(BloId left, BloId right)
{
return !(left == right);
}
public override string ToString()
{
return this._value;
}
}

这些类非常简化,所有不需要的代码都被省略了,因为重点放在真正的问题上。

我创建的映射是直接的(使用github问题的提示):

cfg.CreateMap<Blo, Dto>(MemberList.None)
.EqualityComparison((src, dst) => src.Id.ToString() == dst.Id)
.ForMember(dst => dst.Id, opt => opt.MapFrom(src => src.Id.ToString()));
cfg.CreateMap<Dto, Blo>(MemberList.None)
.EqualityComparison((src, dst) => src.Id == dst.Id.ToString())
.ForMember(dst => dst.Id, opt => opt.MapFrom(src => new BloId(src.Id)));

我从EFCore创建了一个DbContext和以下代码来查找项目:

var mapper = CreateMapper();
await using (var ctx = new MyContext())
{
var idToFind = new BloId("Container-Id 000");
Expression<Func<Blo, bool>> bloFilter = c => c.Id == idToFind;
var dtoFilter = mapper.MapExpression<Expression<Func<Dto, bool>>>(bloFilter);
var found = await ctx.Dtos.FirstOrDefaultAsync(dtoFilter);
}

如果我使用内存中的数据库,这将像预期的那样工作。但是如果我切换到SQLite数据库,会出现以下异常:

The LINQ expression 'DbSet<Dto> .Where(d => new BloId(d.Id) == Container-Id 000)' could not be translated. Either rewrite the query in a form that can be translated, or switch to client evaluation explicitly by inserting a call to either AsEnumerable(), AsAsyncEnumerable(), ToList(), or ToListAsync(). See https://go.microsoft.com/fwlink/?linkid=2101038 for more information.

原因非常清楚:bloFilter

c => (c.Id == value(AutoMapperVsEfCore.Program+<>c__DisplayClass0_0).idToFind)

转化为dtoFilter

c => (new BloId(c.Id) == Container-Id 000)

这就是问题所在!无法在SQL中创建BloId的实例。

预期的dto-filter可能是这样的:c => (c.Id == "Container-Id 000")

但是我完全不知道如何配置AutoMapper来将我指定的blot -filter转换为工作的dto-filter。

我如何创建这样一个过滤器?

我已经尽力了

  • 将bloo过滤器更改为c => c.Id.ToString() == idToFind.ToString(),但这会导致c => (new BloId(c.Id).ToString() == Container-Id 000.ToString())的dto过滤器具有相同的问题。
  • 将id的映射更改为
    cfg.CreateMap<BloId, string>(MemberList.None).ConvertUsing(id => id.ToString());
    cfg.CreateMap<string, BloId>(MemberList.None).ConvertUsing(id => new BloId(id));
    cfg.CreateMap<Blo, Dto>(MemberList.None)
    .EqualityComparison((src, dst) => src.Id.ToString() == dst.Id)
    .ForMember(dst => dst.Id, opt => opt.MapFrom(src => src.Id));
    cfg.CreateMap<Dto, Blo>(MemberList.None)
    .EqualityComparison((src, dst) => src.Id == dst.Id.ToString())
    .ForMember(dst => dst.Id, opt => opt.MapFrom(src => src.Id));
    
    但是这会导致表达式映射期间出现异常,因为类型不兼容。

为完整起见,使用的DbContext为:

public class MyContext : DbContext
{
public const string DatabasePath = @"D:Temptesting.db";
public DbSet<Dto> Dtos { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
//optionsBuilder.UseInMemoryDatabase("testing");
optionsBuilder.UseSqlite(new SqliteConnectionStringBuilder { DataSource = DatabasePath }.ToString());
base.OnConfiguring(optionsBuilder);
}
}

CreateMapper的内容:

private static IMapper CreateMapper()
{
var config = new MapperConfiguration(
cfg =>
{
cfg.CreateMap<Blo, Dto>(MemberList.None)
.EqualityComparison((src, dst) => src.Id.ToString() == dst.Id)
.ForMember(dst => dst.Id, opt => opt.MapFrom(src => src.Id.ToString()));
cfg.CreateMap<Dto, Blo>(MemberList.None)
.EqualityComparison((src, dst) => src.Id == dst.Id.ToString())
.ForMember(dst => dst.Id, opt => opt.MapFrom(src => new BloId(src.Id)));
});
var result = config.CreateMapper();
result.ConfigurationProvider.AssertConfigurationIsValid();
return result;
}

经过大量的研究和无数的测试,我找到了一个可行的解决方案。

我使用一个单独的属性来查询业务逻辑元素:

public class Blo
{
public Blo(BloId id)
{
this.Id = id;
}
public BloId Id { get; }
public string QueryId => this.Id.ToString();
}

数据传输对象不变。映射现在是:

cfg.CreateMap<BloId, string>(MemberList.None).ConvertUsing(id => id.ToString());
cfg.CreateMap<string, BloId>(MemberList.None).ConvertUsing(value => new BloId(value));
cfg.CreateMap<Dto, Blo>(MemberList.None)
.EqualityComparison((src, dst) => src.Id == dst.QueryId)
.ForCtorParam("id", opt => opt.MapFrom(src => src.Id))
.ForMember(dst => dst.QueryId, opt => opt.MapFrom(src => src.Id));
cfg.CreateMap<Blo, Dto>(MemberList.None)
.EqualityComparison((src, dst) => src.QueryId == dst.Id)
.ForMember(dst => dst.Id, opt => opt.MapFrom(src => src.QueryId))
.ForSourceMember(src => src.Id, opt => opt.DoNotValidate());

使用的DbContext仍然相同。

老查询

c => c.Id == idToFind

新查询

c => c.QueryId == idToFind.ToString()

这种方法也适用于查找数组中的项,如:

c => new[] { idToFind.ToString() }.Contains(c.QueryId)

在我找到更好的解决办法之前,这将是我要走的路。

最新更新