使用 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。
如何创建这样的过滤器?
到目前为止我尝试了什么
- 将 blo-filter 更改为
c => c.Id.ToString() == idToFind.ToString()
但这会导致 c => (new BloId(c.Id).ToString() == Container-Id 000.ToString())
的 dto-filter 出现同样的问题。
- 将 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:\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)
在我找到更好的解决方案之前,这将是我的选择。
我使用 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。
如何创建这样的过滤器?
到目前为止我尝试了什么
- 将 blo-filter 更改为
c => c.Id.ToString() == idToFind.ToString()
但这会导致c => (new BloId(c.Id).ToString() == Container-Id 000.ToString())
的 dto-filter 出现同样的问题。 - 将 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:\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)
在我找到更好的解决方案之前,这将是我的选择。