使用 id class 到字符串的自动映射器表达式映射导致 EFCore 中的翻译错误

Using automapper expression mapping of id class to string lead to translation error in EFCore

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

class是:

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;
    }
}

这些 class 非常简单,由于专注于真正的问题,所有不需要的代码都被省略了。

我创建的映射很简单(使用 that github issue 的提示):

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 才能将我指定的 blo-filter 转换为工作 dto-filter。

如何创建这样的过滤器?

到目前为止我尝试了什么

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

public class MyContext : DbContext
{
    public const string DatabasePath = @"D:\Temp\testing.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)

在我找到更好的解决方案之前,这将是我的选择。