AutoMapper Version=13.0.1
Microsoft.EntityFrameworkCore Version=7.0.20
Microsoft.EntityFrameworkCore.InMemory Version=7.0.20< /p>
Ожидаемое поведение
Я хотел бы создать один профиль и объединить работу для одного DTO в памяти и в проекции в БД. Учет специфических требований запросов к БД перевода. У меня есть некоторые проблемы, вы можете увидеть более подробную информацию на этапах воспроизведения. Есть подробные комментарии к текущему и ожидаемому поведению, а также возможные решения.
Test1() — Базовая настройка профиля и выявление проблем, с которыми я столкнулся. Этот профиль хорошо работает в ProjectTo, но плохо работает в памяти с Map, поскольку в Expression нет проверок на null.
Test2() — хорошо работает в памяти Map, но не работает. в ProjectTo. Причины понятны — использование Func, который не является выражением и не может быть переведен в SQL.
Test3(). Одно из решений — всегда использовать AsQueryable и Projection, даже в памяти, чтобы закрытие работало. Но, на мой взгляд, это тоже хак, а не решение и у него есть свои проблемы
Test4() — Единственное возможное решение, которое пришло мне в голову — создать объект унаследован от Dto и создайте для него отдельный профиль и используйте его ТОЛЬКО для Projection, а основной профиль используйте только в памяти. Это работает. Но это тоже далеко от идеала, всегда нужно помнить, что один Dto должен использоваться только в памяти, а другой только в Projection. Также выполните преобразования из FooDtoForProjection в FooDto. Также поддерживайте оба профиля при внесении изменений в модель БД.
Какое наиболее предпочтительное решение такой проблемы?
Из моих мыслей хотелось бы создать 2 карты для одну и ту же пару Source и Destination, чтобы одна карта использовалась для работы в памяти, а другая в Projection. Но Automapper этого не позволяет. Или, если бы можно было сделать 2 отдельных MapFroms. Например, opt.MapFromForMemory и opt.MapFromForProjection. Да, действительно, Automapper покрывает 99% необходимой работы, но я попал в тот 1%, который он не покрывает и пока не вижу хорошего и красивого решения. Помогите советом!
PS Я понимаю, что в Expression можно добавить проверки, но если будет много языков и полей для перевода, то это существенно увеличит размер и сложность приложения. результирующий SQL-запрос
using AutoMapper;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics;
try
{
await Helper.Populate();
await Helper.Test1();
await Helper.Test2();
await Helper.Test3();
await Helper.Test4();
}
catch (Exception ex)
{
}
public static class Helper
{
public static FooContext CreateContext()
{
DbContextOptionsBuilder builder =
new DbContextOptionsBuilder()
.UseInMemoryDatabase("foo_in_memory")
.ConfigureWarnings(b => b.Ignore(InMemoryEventId.TransactionIgnoredWarning));
FooContext dbContext = new FooContext(builder.Options);
return dbContext;
}
public static async Task Populate()
{
FooContext dbContext = CreateContext();
await dbContext.Database.EnsureCreatedAsync();
Foo foo = new Foo();
foo.Name = "Foo";
foo.Translate = new List
{
new()
{
Culture = "en",
Name = "FooEn"
},
new()
{
Culture = "fr",
Name = "FooFr"
},
};
dbContext.Add(foo);
await dbContext.SaveChangesAsync();
await dbContext.DisposeAsync();
}
// Works on the DB side, but does not work in memory
public static async Task Test1()
{
MapperConfiguration mapperConfiguration =
new MapperConfiguration(expression =>
{
expression.AddProfile(new FooProfile());
});
IMapper mapper = mapperConfiguration.CreateMapper();
// 1. Projection
// This works because the expression is transformed to SQL and all Null checks
// are performed on the DB side and the correct value is returned
FooContext dbContext_1 = CreateContext();
IQueryable query_1 = dbContext_1.FooDbSet;
IQueryable queryProjection_1 =
mapper.ProjectTo(query_1, new { culture = "fr" });
FooDto dto_1 = await queryProjection_1.FirstAsync();
await dbContext_1.DisposeAsync();
// dto_1.Name = "FooFr" - Working
// 2. Map with Include
// The closure does not work. culture in the profile = null.
// When executed in memory (not on the DB side),
// expression throws a NullReferenceException and
// Automapper simply swallows it and the Name field remains Null.
// Why does the closure not work with Map?
FooContext dbContext_2 = CreateContext();
IQueryable query_2 = dbContext_2.FooDbSet.Include(q => q.Translate);
Foo foo_2 = await query_2.FirstAsync();
FooDto dto_2 = mapper.Map(foo_2,
options => options.Items["culture"] = "fr");
await dbContext_2.DisposeAsync();
//dto_2.Name = null - NOT working
// 3. Map with Include and BeforeMap
// The situation in point 2 can be solved by adding BeforeMap to the profile,
// but this looks more like a crutch
MapperConfiguration mapperConfigurationWithBeforeMap =
new MapperConfiguration(expression =>
{
expression.AddProfile(new FooProfileWithBeforeMap());
});
IMapper mapperWithBeforeMap = mapperConfigurationWithBeforeMap.CreateMapper();
FooContext dbContext_3 = CreateContext();
IQueryable query_3 = dbContext_3.FooDbSet.Include(q => q.Translate);
Foo foo_3 = await query_3.FirstAsync();
FooDto dto_3 = mapperWithBeforeMap.Map(foo_3,
options => options.Items["culture"] = "fr");
await dbContext_3.DisposeAsync();
//dto_3.Name = "FooFr" - NOT working
// 4. Map WITHOUT Include and BeforeMap
// But even the crutch from point 3 does
// not save from the situation when the required translation
// is not available (it is simply not translated into the required language
// or was not requested from the Include DB).
// The default name from foo.Name is not returned because
// expression in memory throws a NullReferenceException after FirstOrDefault()
FooContext dbContext_4 = CreateContext();
IQueryable query_4 = dbContext_4.FooDbSet;
Foo foo_4 = await query_4.FirstAsync();
FooDto dto_4 = mapperWithBeforeMap.Map(foo_4,
options => options.Items["culture"] = "fr");
await dbContext_4.DisposeAsync();
//dto_4.Name = null - NOT working
// 5. Map With Include and BeforeMap not exists translate
string notExistsCulture = "ua";
FooContext dbContext_5 = CreateContext();
IQueryable query_5 = dbContext_5.FooDbSet.Include(q => q.Translate);
Foo foo_5 = await query_5.FirstAsync();
FooDto dto_5 = mapperWithBeforeMap.Map(foo_5,
options => options.Items["culture"] = notExistsCulture);
await dbContext_5.DisposeAsync();
//dto_5.Name = null - NOT working
}
// Works in memory (with hacks - BeforeMap and set culture), but does not work on DB side
public static async Task Test2()
{
MapperConfiguration mapperConfiguration =
new MapperConfiguration(expression =>
{
expression.AddProfile(new FooProfileWithUseFunc());
});
IMapper mapper = mapperConfiguration.CreateMapper();
// 1. Projection
// Does not work for the obvious reason of using Func rather
// than Expression when building a profile map
FooContext dbContext_1 = CreateContext();
IQueryable query_1 = dbContext_1.FooDbSet;
IQueryable queryProjection_1 =
mapper.ProjectTo(query_1, new { culture = "fr" });
FooDto dto_1 = await queryProjection_1.FirstAsync();
await dbContext_1.DisposeAsync();
// dto_1.Name = "Foo" - expected "FooFr" - NOT working
// 2. Map with Include
// The closure does not work. culture in the profile = null.
// Why does the closure not work with Map?
FooContext dbContext_2 = CreateContext();
IQueryable query_2 = dbContext_2.FooDbSet.Include(q => q.Translate);
Foo foo_2 = await query_2.FirstAsync();
FooDto dto_2 = mapper.Map(foo_2,
options => options.Items["culture"] = "fr");
await dbContext_2.DisposeAsync();
//dto_2.Name = "Foo" - expected "FooFr" - NOT working
// 3. Map with Include and BeforeMap
// The situation in point 2 can be solved by adding BeforeMap to the profile,
// but this looks more like a crutch
MapperConfiguration mapperConfigurationWithBeforeMap =
new MapperConfiguration(expression =>
{
expression.AddProfile(new FooProfileUseFuncWithBeforeMap());
});
IMapper mapperWithBeforeMap = mapperConfigurationWithBeforeMap.CreateMapper();
FooContext dbContext_3 = CreateContext();
IQueryable query_3 = dbContext_3.FooDbSet.Include(q => q.Translate);
Foo foo_3 = await query_3.FirstAsync();
FooDto dto_3 = mapperWithBeforeMap.Map(foo_3,
options => options.Items["culture"] = "fr");
await dbContext_3.DisposeAsync();
//dto_3.Name = "FooFr" - working (hack)
// 4. Map WITHOUT Include and BeforeMap
FooContext dbContext_4 = CreateContext();
IQueryable query_4 = dbContext_4.FooDbSet;
Foo foo_4 = await query_4.FirstAsync();
FooDto dto_4 = mapperWithBeforeMap.Map(foo_4,
options => options.Items["culture"] = "fr");
await dbContext_4.DisposeAsync();
//dto_4.Name = "Foo" - working
// 5. Map With Include and BeforeMap not exists translate
string notExistsCulture = "ua";
FooContext dbContext_5 = CreateContext();
IQueryable query_5 = dbContext_5.FooDbSet.Include(q => q.Translate);
Foo foo_5 = await query_5.FirstAsync();
FooDto dto_5 = mapperWithBeforeMap.Map(foo_5,
options => options.Items["culture"] = notExistsCulture);
await dbContext_5.DisposeAsync();
//dto_5.Name = "Foo" - working
}
// One solution is to use AsQueryable and Projection always,
// even in memory, so that the closure works.
// But, in my opinion, this is also a hack,
// not a solution and it has its own problems
public static async Task Test3()
{
MapperConfiguration mapperConfiguration =
new MapperConfiguration(
expression =>
{
expression.AddProfile(new FooProfile());
});
IMapper mapper = mapperConfiguration.CreateMapper();
// 1. Projection
FooContext dbContext_1 = CreateContext();
IQueryable query_1 = dbContext_1.FooDbSet;
IQueryable queryProjection_1 =
mapper.ProjectTo(query_1, new { culture = "fr" });
FooDto dto_1 = await queryProjection_1.FirstAsync();
await dbContext_1.DisposeAsync();
// dto_1.Name = "FooFr" - working
// 2. Map with Include
FooContext dbContext_2 = CreateContext();
IQueryable query_2 = dbContext_2.FooDbSet.Include(q => q.Translate);
List list_2 = await query_2.ToListAsync();
IQueryable queryProjection_2 =
mapper.ProjectTo(list_2.AsQueryable(), new { culture = "fr" });
FooDto dto_2 = queryProjection_2.First();
await dbContext_2.DisposeAsync();
//dto_2.Name = "FooFr" - working
// 3. Map with Include and BeforeMap
// this test is not needed here
// 4. Map WITHOUT Include
FooContext dbContext_4 = CreateContext();
IQueryable query_4 = dbContext_4.FooDbSet;
List list_4 = await query_4.ToListAsync();
IQueryable queryProjection_4 =
mapper.ProjectTo(list_4.AsQueryable(), new { culture = "fr" });
try
{
// in this case, AsQueryable is still executed in memory, not on the DB side,
// so WITHOUT checking for Null in expression we get a NullReferenceException
FooDto dto_4 = queryProjection_4.First();
}
catch (Exception ex)
{
}
await dbContext_4.DisposeAsync();
//Exception - NOT working
// 5. Map With Include and BeforeMap not exists translate
string notExistsCulture = "ua";
FooContext dbContext_5 = CreateContext();
IQueryable query_5 = dbContext_5.FooDbSet.Include(q => q.Translate);
List list_5 = await query_5.ToListAsync();
IQueryable queryProjection_5 =
mapper.ProjectTo(list_5.AsQueryable(), new { culture = notExistsCulture });
try
{
// in this case, AsQueryable is still executed in memory, not on the DB side,
// so WITHOUT checking for Null in expression we get a NullReferenceException
FooDto dto_5 = queryProjection_5.First();
}
catch (Exception ex)
{
}
await dbContext_5.DisposeAsync();
//Exception - NOT working
}
// The only possible solution that came to my mind is to create an object inherited
// from Dto and create a separate profile for it and use it
// ONLY for Projection, and use the main profile only in memory.
// This works.
// But it is also far from ideal,
// you must always remember that one Dto should be used
// only in memory, and the other only in Projection.
// Also perform transformations from FooDtoForProjection => FooDto.
// Also support both profiles when making changes to the DB model
public static async Task Test4()
{
MapperConfiguration mapperConfiguration = new MapperConfiguration(expression =>
{
expression.AddProfile(new FooProfileForMemory());
expression.AddProfile(new FooProfileForProjection());
});
IMapper mapper = mapperConfiguration.CreateMapper();
// 1. Projection
FooContext dbContext_1 = CreateContext();
IQueryable query_1 = dbContext_1.FooDbSet;
IQueryable queryProjection_1 =
mapper.ProjectTo(query_1, new { culture = "fr" });
FooDto dto_1 = await queryProjection_1.FirstAsync();
await dbContext_1.DisposeAsync();
// dto_1.Name = "FooFr" - Working
// 2. Map with Include
FooContext dbContext_2 = CreateContext();
IQueryable query_2 = dbContext_2.FooDbSet.Include(q => q.Translate);
Foo foo_2 = await query_2.FirstAsync();
FooDto dto_2 =
mapper.Map(foo_2, options => options.Items["culture"] = "fr");
await dbContext_2.DisposeAsync();
// dto_2.Name = "FooFr" - Working
// 3. Map with Include and BeforeMap
// this test is not needed here
// 4. Map WITHOUT Include
FooContext dbContext_4 = CreateContext();
IQueryable query_4 = dbContext_4.FooDbSet;
Foo foo_4 = await query_4.FirstAsync();
FooDto dto_4 =
mapper.Map(foo_4, options => options.Items["culture"] = "fr");
await dbContext_4.DisposeAsync();
//dto_4.Name = "Foo" - working
// 5. Map With Include not exists translate
string notExistsCulture = "ua";
FooContext dbContext_5 = CreateContext();
IQueryable query_5 = dbContext_5.FooDbSet.Include(q => q.Translate);
Foo foo_5 = await query_5.FirstAsync();
FooDto dto_5 =
mapper.Map(foo_5, options => options.Items["culture"] = notExistsCulture);
await dbContext_5.DisposeAsync();
//dto_5.Name = "Foo" - working
}
}
public class FooProfile : Profile
{
public FooProfile()
{
string? culture = null;
CreateMap()
.ForMember(
desc => desc.Name,
opt => opt.MapFrom(src =>
src.Translate.Any(q => q.Culture == culture && q.Name != null)
? src.Translate.First(q => q.Culture == culture).Name
: src.Name)
);
}
}
public class FooProfileWithBeforeMap : Profile
{
public FooProfileWithBeforeMap()
{
string? culture = null;
CreateMap()
.ForMember(
desc => desc.Name,
opt => opt.MapFrom(src =>
src.Translate.FirstOrDefault(q => q.Culture == culture).Name
?? src.Name))
.BeforeMap((foo, dto, context) =>
{
culture = context.Items["culture"]?.ToString();
});
}
}
public class FooProfileWithUseFunc : Profile
{
public FooProfileWithUseFunc()
{
string? culture = null;
CreateMap()
.ForMember(
desc => desc.Name,
opt => opt.MapFrom((src, dest) =>
{
return src.Translate.FirstOrDefault(q => q.Culture == culture)?.Name
?? src.Name;
}));
}
}
public class FooProfileUseFuncWithBeforeMap : Profile
{
public FooProfileUseFuncWithBeforeMap()
{
string? culture = null;
CreateMap()
.ForMember(
desc => desc.Name,
opt => opt.MapFrom((src, dest) =>
{
return src.Translate.FirstOrDefault(q => q.Culture == culture)?.Name
?? src.Name;
}))
.BeforeMap((foo, dto, context) =>
{
culture = context.Items["culture"]?.ToString();
});
}
}
public class Foo
{
public int Id { get; set; }
public string Name { get; set; } = null!;
public List Translate { get; set; } = new();
}
public class FooTranslate
{
public int Id { get; set; }
public int FooId { get; set; }
public string? Culture { get; set; }
public string? Name { get; set; }
public string? Description { get; set; }
public Foo? Foo { get; set; }
}
public class FooDto
{
public string Name { get; set; } = null!;
}
public class FooDtoForProjection : FooDto
{
}
public class FooProfileForMemory : Profile
{
public FooProfileForMemory()
{
CreateMap()
.ForMember(
desc => desc.Name,
opt => opt.MapFrom((src, dest, _, context) =>
{
return src.Translate
.FirstOrDefault(q =>
q.Culture == context.Items["culture"]?.ToString())?.Name
?? src.Name;
}));
}
}
public class FooProfileForProjection : Profile
{
public FooProfileForProjection()
{
string? culture = null;
CreateMap()
.ForMember(
desc => desc.Name,
opt => opt.MapFrom(src =>
src.Translate.FirstOrDefault(q => q.Culture == culture).Name
?? src.Name));
}
}
public class FooContext : DbContext
{
public FooContext(DbContextOptions options) : base(options)
{
}
public DbSet FooDbSet { get; set; } = null!;
public DbSet FooTranslateDbSet { get; set; } = null!;
}
Подробнее здесь: https://stackoverflow.com/questions/788 ... -projectto
Проблемы AutoMapper с одним профилем для Map и ProjectTo ⇐ C#
-
- Похожие темы
- Ответы
- Просмотры
- Последнее сообщение