Проблемы AutoMapper с одним профилем для Map и ProjectToC#

Место общения программистов C#
Ответить Пред. темаСлед. тема
Anonymous
 Проблемы AutoMapper с одним профилем для Map и ProjectTo

Сообщение Anonymous »

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
Реклама
Ответить Пред. темаСлед. тема

Быстрый ответ

Изменение регистра текста: 
Смайлики
:) :( :oops: :roll: :wink: :muza: :clever: :sorry: :angel: :read: *x)
Ещё смайлики…
   
К этому ответу прикреплено по крайней мере одно вложение.

Если вы не хотите добавлять вложения, оставьте поля пустыми.

Максимально разрешённый размер вложения: 15 МБ.

  • Похожие темы
    Ответы
    Просмотры
    Последнее сообщение

Вернуться в «C#»