Как заменить столбцы в Queryable <T>обобщенным образом c без выполнения Queryable - PullRequest
4 голосов
/ 20 марта 2020

В качестве примера у меня есть сущность Product, ProductViewModel и сущность Label. Два из моих свойств продукта соответствуют коду этикетки, а не фактическому значению. Так, например, «Название продукта» - это «code1234», что соответствует метке, в которой «code1234» является кодом, а «Milk» - значением. Метка не присоединяется как внешний ключ. Мы используем AutoMapper для проекций.

public class Product{
    public int ProductId {get; set;}
    public string Name {get; set;}
    public string Description {get; set;}
}

public class ProductViewModel{
    public int ProductId {get; set;}
    public string Name {get; set;}
    public string Description {get; set;}
}

public class Label{
    public int LabelId {get; set;}
    public string Code {get; set;}
    public string Value {get; set;}
    public int LanguageId {get; set}
}

Я ищу способ заменить

var currentLanguageId = 1; //As an example
IQueryable<Product> queryFromDb;

var Products = queryFromDb
    .ProjectTo<ProductViewModel>().AsEnumerable();

foreach(var Product in Products) {
    Product.Name = db.Labels.Where(x => x.Code == Product.Name && x.LanguageId == currentLanguageId).Single().Value;
    Product.Description = db.Labels.Where(x => x.Code == Product.Description && x.LanguageId == currentLanguageId).Single().Value;
}

кодом, который будет держать запрос отложенным с момента фильтрации и сортировка выполняется по значению метки, соответствующему названию и описанию продукта, а не по самим наименованиям и описаниям, которые являются бессмысленными кодами.

Тогда мне также понадобится способ сделать все это обобщенным c, поскольку многие объекты в нашей базе данных имеют свойства, которые являются кодами меток.

То, что у меня есть до сих пор:

var result = scope.GetRepository<Product>().GetAll() //returns Queryable<Product>
                .ProjectTo<ProductViewModel>(_mapper.ConfigurationProvider) //returns Queryable<ProductViewModel>
                .WithLabels(_mapper, scope, x => x.Name, x => x.Description) //returns Queryable with columns replaced with a query
                .ToDataResult(request); //sorts filters takes skips, etc.
public static IQueryable<T> WithLabels<T>(this IQueryable<T> instance,
    IMapper mapper,
    IBaseReadContextScope scope,
    params Expression<Func<T, string>>[] expressions) where T : class
{
    var currentLanguage = 1; //as an example
    var labels = scope.GetRepository<Label>().GetAll(x => x.Language == currentLanguageId);
    foreach (var expression in expressions)
    {
        var query = instance
                .GroupJoin(
                    labels,
                    expression,
                    label => label.Code,
                    (x, y) => new { Obj = x, Label = y })
                .SelectMany(
                    xy => xy.Label.DefaultIfEmpty(),
                    (x, y) => new { Obj = x.Obj, Label = y })
                .Select(s => new ObjectWithLabel<T>()
                {
                    Object = s.Obj,
                    Label = s.Label
                });
        instance = mapper.ProjectTo<T>(query, new { propertyName = ExpressionUtilities.PropertyName(expression) });
    }

    return instance;
}
CreateMap<ObjectWithLabel<ProductViewModel>, ProductViewModel>()
    .ForMember(x => x.Name, m =>
    {
        m.Condition(x => propertyName == nameof(ProductViewModel.Name));
        m.MapFrom(x => x.Label.Value);
    })
    .ForMember(x => x.Name, m =>
    {
        m.Condition(x => propertyName != nameof(ProductViewModel.Name));
        m.MapFrom(x => x.Object.Name);
    })
    .ForMember(x => x.Description, m =>
    {
        m.Condition(x => propertyName == nameof(ProductViewModel.Description));
        m.MapFrom(x => x.Label.Value);
    })
    .ForMember(x => x.Description, m =>
    {
        m.Condition(x => propertyName != nameof(ProductViewModel.Description));
        m.MapFrom(x => x.Object.Description);
    });

, которое фактически работает для одного свойства, но не для нескольких. Вдобавок ко всему, необходимость проецировать туда и обратно из ProductViewModel и ObjectWithLabel не велика. Причина, по которой я использую .Condition вместо простого троичного оператора, заключается в том, что ProjectTo не поддерживает выражения, и у меня возникают проблемы с работой UseAsDataSource () (что перевело бы это выражение для нас)

Идеи на данный момент :

  1. Использовать перехватчики запросов. С помощью AutoMapper мы будем создавать строку, такую ​​как INTERCEPTME_ColumnName_Code_Language, и заменять эту строку запросом (либо написанным в LINQ, либо в форме функции SQL). Кажется, что это будет работать, но есть и недостаток в том, что он не работает для модульных тестов с NO SQL (вероятно), и ему необходимо проверять каждый входящий запрос (если только нет способа пометить запрос как «перехват»).

  2. Используйте метод, аналогичный моему текущему методу WithLabels, но создайте единственное объединение с несколькими столбцами внутри него, затем проецируйте только один раз из ObjectWithLabel s в ProductViewModel. (Понятия не имею, как обобщенное c объединение с неизвестным числом столбцов будет выглядеть так:

  3. Найдите способ прямой замены столбца без использования промежуточного объекта / проекции путем создания запросов и отправка их в качестве параметра в AutoMapper, быстрая иллюстрация базовой идеи c:

Dictionary<string, IQueryable<string>> dictionary = null;

CreateMap<Product, ProductViewModel>()
    .ForMember(x => x.Name, m => m.MapFrom(x => 
        dictionary["Name"]))
    .ForMember(x => x.Description, m => m.MapFrom(x => 
        dictionary["Description"])));

Где будет построен словарь с запросами, которые возвращают значение Label.Value, соответствующее коду и желаемый язык.

Буду признателен за любой вклад в базовую проблему, которая заключается в замене столбца объекта в запросе, без выполнения этого запроса, а также с любым из потенциальных решений, которые я упомянул.

Спасибо

Ответы [ 3 ]

5 голосов
/ 23 марта 2020

Я не уверен, правильно ли я сформулировал вашу формулировку проблемы, и думаю, что в комментариях к LINQ я пошел совершенно неверно.

Похоже, вы пытаетесь соединить две таблицы из БД (и примените фильтр LanguageId к одному из них). Я полагаю, что с EF.Core 3 (я полагаю, вы используете последнюю версию) вам не нужно определять FK для объединения таблиц:

var Products = db.Products.Join(
                    db.Labels.Where(l => l.LanguageId == 1), product => product.Name,
                    label => label.Code,
                    (p, l) => new {p, l})
                .Join(db.Labels.Where(l => l.LanguageId == 1), p => p.p.Description, l => l.Code,
                    (pp, l) => new ProductViewModel { Name = pp.l.Value, ProductId = pp.p.ProductId, Description = l.Value});

дает следующие значения SQL:

SELECT [t].[Value] AS [Name], [p].[ProductId], [t0].[Value] AS [Description]
FROM [Products] AS [p]
INNER JOIN (
    SELECT [l].[LabelId], [l].[Code], [l].[LanguageId], [l].[Value]
    FROM [Labels] AS [l]
    WHERE [l].[LanguageId] = 1
) AS [t] ON [p].[Name] = [t].[Code]
INNER JOIN (
    SELECT [l0].[LabelId], [l0].[Code], [l0].[LanguageId], [l0].[Value]
    FROM [Labels] AS [l0]
    WHERE [l0].[LanguageId] = 1
) AS [t0] ON [p].[Description] = [t0].[Code]

Как видите, таким образом может быть проще создать ProductViewModel прямо внутри оператора select.

2 голосов
/ 23 марта 2020

Решением было получить хранилище меток и передать его в качестве параметра при вызове преобразователя и создать запрос метки непосредственно в профиле автоматического обработчика.

Использование:

var labels = scope.GetRepository<Label>().GetAll().Where(x => x.LanguageId == 1)
var result = scope.GetRepository<Product>().GetAll() //returns Queryable<Product>
                .ProjectTo<ProductViewModel>(_mapper.ConfigurationProvider, new {labels})
                .Orderby(x => x.Description)
                .Take(10);

Профиль сопоставления:

IQueryable<Label> labels = null;
CreateMap<Product, ProductViewModel>()
    .ForMember(x => x.Name, m =>
    {
        m.MapFrom(x => (from label in labels
                where label.Code == x.Name
                select label.Value).First());
    })
    .ForMember(x => x.Description, m =>
    {
        m.MapFrom(x => (from label in labels
                where label.Code == x.Description
                select label.Value).First());
    });

Вероятно, MapFrom можно поместить в метод MapFromLabel, который позволит избежать повторения кода.

1 голос
/ 29 марта 2020

Как я вижу, вы используете AutoMapper непосредственно на уровне доступа к данным с помощью оператора ProjectTo.

Один альтернативный и обобщенный c подход для прямого возврата требуемой модели будет

 var labelQuery = db.Labels.Where(x => x.LanguageId == 1);// 1 is passed parameter.
            var Products = db.Products.Select(x => new ProductViewModel()
            {
                ProductId = x.ProductId,
                Name = labelQuery.FirstOrDefault(l => l.Code == x.Name).Name,
                Description = labelQuery.FirstOrDefault(l => l.Code== x.Description).Value
          }).ToList();

Надеется, что это решит проблему

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...