Динамический linq-фильтр дочерних элементов с использованием pivot - PullRequest
0 голосов
/ 12 мая 2019

У меня есть бизнес-объекты, которые выглядят следующим образом:

class Project
{
    public int ID
    {
        get;set;
    }

    public string ProjectName
    {
        get;set;
    }

    public IList<ProjectTag> ProjectTags
    {
        get;set;
    }
}

class ProjectTag
{
    public int ID
    {
        get;set;
    }

    public int ProjectID
    {
        get;set;
    }

    public string Name 
    {
        get;set;
    }

    public string Value
    {
        get;set;
    }
}

Пример данных:

Project:
ID    ProjectName
1     MyProject

ProjectTags:
ID    ProjectID    Name     Value
1     1            Name 1   Value 1
2     1            Name 2   Value 2
3     1            Name 3   Value 3

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

То, что я пытаюсь сделать, - это дать нашим пользователям возможность выбирать проекты на основе критериев поиска с использованием System.Linq.Dynamic. Например, чтобы выбрать только проект в моем примере выше, наши пользователи могут ввести это:

ProjectName == "MyProject"

Более сложным аспектом является применение фильтра к ProjectTags. В настоящее время наше приложение позволяет пользователям делать это для фильтрации проектов по их тегам Project:

ProjectTags.Any(Name == "Name 1" and Value == "Value 1")

Это работает, но начинает становиться немного грязным для конечных пользователей. В идеале я хотел бы написать что-нибудь, что позволило бы им сделать следующее:

Name 1 == "Value 1"

Или, если необходимо (из-за пробелов в имени), что-то вроде следующего ...

[Name 1] == "Value 1"
"Name 1" == "Value 1"

Из-за отсутствия лучшего объяснения мне кажется, что я хочу сделать эквивалент SQL-элемента в ProjectTags, а затем все еще иметь возможность выполнить предложение where для этого. Я рассмотрел некоторые вопросы в StackOverflow о поворотах и ​​динамическом повороте, но я не нашел ничего слишком полезного.

Я также думал о циклическом просмотре всех имен ProjectTag и построении динамического запроса с использованием левого соединения для каждого. Я думаю, что-то вроде этого:

select 
    Project.*, 
    Name1Table.Value [Name 1],
    Name2Table.Value [Name 2],
    Name3Table.Value [Name 3]
from
    Project
    left join ProjectTag Name1Table on Name = 'Name 1'
    left join ProjectTag Name2Table on Name = 'Name 2'
    left join ProjectTag Name3Table on Name = 'Name 3'

А затем возьмите этот запрос и примените к нему предложение where. Но я не совсем уверен, как это сделать в Linq, а также как справиться с пробелами в имени.

Я также сталкивался с ExpandoObject. Я подумал, что, возможно, смогу конвертировать Project в ExpandoObject. Затем выполните цикл по всем известным именам ProjectTag, добавив каждое имя в ExpandoObject и, если у этого Project есть ProjectTag для этого имени, используйте это значение ProjectTag в качестве значения, иначе пустую строку. Например ...

    private static object Expand(
        Project project,
        List<string> projectTagNames)
    {
        var expando = new ExpandoObject();
        var dictionary = (IDictionary<string, object>) expando;

        foreach (var property in project.GetType()
            .GetProperties())
        {
            dictionary.Add(property.Name, property.GetValue(project));
        }

        foreach (var tagName in projectTagNames)
        {
            var tagValue = project.ProjectTags.SingleOrDefault(p => p.Name.Equals(tagName));
            dictionary.Add(tagName, tagValue?.Value ?? "");
        }

        return expando;
    }

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

Затем, конечно, я обнаружил, что динамический linq плохо работает с ExpandoObject, и поэтому он не может найти динамические свойства. Я предполагаю, что это потому, что он по существу имеет тип Object, который не собирается определять какие-либо динамические свойства. Может быть, возможно создать тип во время выполнения, который соответствует? Даже если это работает, я не думаю, что это может объяснить пробелы в Имени.

Пытаюсь ли я добиться слишком многого с помощью этой функции? Должен ли я просто сказать пользователям использовать синтаксис, такой как ProjectTags.Any (Name == "Name1" и Value == "Value1")? Или есть какой-то способ обмануть динамический linq в понимании ExpandoObject? Похоже, иметь способ переопределить способ, которым динамический linq разрешает имена свойств, было бы очень удобно.

1 Ответ

0 голосов
/ 16 мая 2019

Как насчет использования переводчика для преобразования ссылок тегов?

Я предполагаю, что имена тегов, содержащие пробелы, будут заключены в квадратные скобки ([]), а имена полей Project являются известным списком.

public static class TagTranslator {
    public static string Replace(this string s, Regex re, string news) => re.Replace(s, news);
    public static string Surround(this string src, string beforeandafter) => $"{beforeandafter}{src}{beforeandafter}";
    public static string SurroundIfMissing(this string src, string beforeandafter) => (src.StartsWith(beforeandafter) && src.EndsWith(beforeandafter)) ? src : src.Surround(beforeandafter);

    public static string Translate(string q) {
        var projectFields = new[] { "ID", "ProjectName", "ProjectTags" }.ToHashSet();

        var opREStr = @"(?<op>==|!=|<>|<=|>=|<|>)";
        var revOps = new[] {
            new { Fwd = "==", Rev = "==" },
            new { Fwd = "!=", Rev = "!=" },
            new { Fwd = "<>", Rev = "<>" },
            new { Fwd = "<=", Rev = ">=" },
            new { Fwd = ">=", Rev = "<=" },
            new { Fwd = "<", Rev = ">" },
            new { Fwd = ">", Rev = "<" }
        }.ToDictionary(p => p.Fwd, p => p.Rev);

        var openRE = new Regex(@"^\[", RegexOptions.Compiled);
        var closeRE = new Regex(@"\]$", RegexOptions.Compiled);

        var termREStr = @"""[^""]+""|(?:\w|\.)+|\[[^]]+\]";
        var term1REStr = $"(?<term1>{termREStr})";
        var term2REStr = $"(?<term2>{termREStr})";
        var wsREStr = @"\s?";
        var exprRE = new Regex($"{term1REStr}{wsREStr}{opREStr}{wsREStr}{term2REStr}", RegexOptions.Compiled);

        var tq = exprRE.Replace(q, m => {
            var term1 = m.Groups["term1"].Captures[0].Value.Replace(openRE, "").Replace(closeRE, "");
            var term1q = term1.SurroundIfMissing("\"");
            var term2 = m.Groups["term2"].Captures[0].Value.Replace(openRE, "").Replace(closeRE, "");
            var term2q = term2.SurroundIfMissing("\"");
            var op = m.Groups["op"].Captures[0].Value;
            if (!projectFields.Contains(term1) && !term1.StartsWith("\"")) { // term1 is Name, term2 is Value
                return $"ProjectTags.Any(Name == {term1q} && Value {op} {term2})";
            }
            else if (!projectFields.Contains(term2) && !term2.StartsWith("\"")) { // term2 is Name, term1 is Value
                return $"ProjectTags.Any(Name == {term2q} && Value {revOps[op]} {term1})";
            }
            else
                return m.Value;
        });
        return tq;
    }
}

Теперь вы просто переводите свой запрос:

var q = "ProjectName == \"Project1\" && [Name 1] == \"Value 1\" && [Name 3] == \"Value 3\"";
var tq = TagTranslator.Translate(q);
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...