使用枢轴的动态linq过滤器子级

时间:2019-05-12 19:05:41

标签: c# linq dynamic

我有一些如下的业务对象:

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

基本上,这是我们的用户在Project上定义自己的列的一种方式。因此,重要的是要记住,在设计时我不知道ProjectTag条目的名称。

我要完成的工作是使我们的用户能够使用System.Linq.Dynamic根据搜索条件选择项目。例如,要在上面的示例中仅选择项目,我们的用户可以输入以下内容:

ProjectName == "MyProject"

更复杂的方面是将过滤器应用于ProjectTag。我们的应用程序当前允许用户执行此操作,以便通过其ProjectTag过滤项目:

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

可以,但是开始变得有些混乱,供最终用户使用。理想情况下,我想写一些能让他们做以下事情的东西:

Name 1 == "Value 1"

或者,如果有必要(由于名称中有空格),则类似于以下内容...

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

由于缺乏更好的解释,似乎我想在ProjectTags上执行等效的SQL透视,然后仍然能够执行针对该语句的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 :(得分:0)

如何使用翻译器转换标签引用?

我假设包含空格的标签名称将用方括号([])包围,并且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);