我使用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)
在我找到更好的解决办法之前,这将是我要走的路。