在属性列表上构建表达式树

时间:2019-09-15 17:31:15

标签: .net dynamic lambda expression azure-cosmosdb

背景和问题

我有一个网站公开了自定义报告生成功能,用户可以在其中动态选择他们想要包含在报告中的字段。 这些字段映射到CosmosDb中存储的文档中的属性。单个文档平均约4kb。典型的报告可能包含100到10k多个文档,具体取决于日期范围标准和租户拥有的数据量。

文档包含类似于以下内容的嵌套关系:

public class Root
{
    public string BusinessId { get; set; }

    public bool SomeBoolean { get; set; }

    public DateTime MyDateTime { get; set; }

    public List<MyNested> MyNestedItems { get; set; }
}

public class MyNested
{
    public DateTime SomeDate { get; set; }

    public int SomeInteger { get; set; }

    public string SomeString { get; set; }
}

如果用户仅选择 MyObject 字段,则最终结果是每个文档只有一个报告行。如果用户选择 MyNestedObject 字段,则最终结果是每个 MyNestedObject 的报告行。在这种情况下, MyObject 字段的数据点将在每行重复。

我当前的实现是从CosmosDb返回整个文档,然后在代码中对结果进行整形以仅匹配用户选择的字段。这是我要解决的过度获取问题。

可能的解决方案

我试图基于这样的输入来构建动态投影:

public class Search
{
    public string BusinessId { get; set; }

    public RootFieldsToInclude RootFieldsToInclude { get; set; }

    public MyNestedFieldsToInclude MyNestedFieldsToInclude { get; set; }
}

public class RootFieldsToInclude
{
    public bool BusinessId { get; set; }

    public bool SomeBoolean { get; set; }

    public bool MyDateTime { get; set; }
}
public class MyNestedFieldsToInclude
{
    public bool SomeDate { get; set; }

    public bool SomeInteger { get; set; }

    public bool SomeString { get; set; }
}

在搜索请求上标记为true的布尔字段将驱动属性包含在对CosmosDb的请求中。

public class MyRepo
{
    private readonly DocumentClient _client;

    public MyRepo()
    {
        _client = new DocumentClient(new Uri("https://xxxxxxxx.documents.azure.com:443/"), "xxxxxxxx");
    }

    const string DatabaseName = "TransactionDb";
    const string CollectionName = "Roots";

    public async Task<IEnumerable<dynamic>> GetDataAsync()
    {
        var search = new Search
        {
            BusinessId = "BBBBB",
            RootFieldsToInclude = new RootFieldsToInclude
            {
                BusinessId = true,
                MyDateTime = false,
                SomeBoolean = true,
            },
            MyNestedFieldsToInclude = new MyNestedFieldsToInclude
            {
                SomeDate = false,
                SomeInteger = false,
                SomeString = true
            }
        };

        var query = _client.CreateDocumentQuery<Root>(UriFactory.CreateDocumentCollectionUri(DatabaseName, CollectionName))
                           .Where(x => x.BusinessId == search.BusinessId)
                          // Example of what query would look like given example search
                          // .Select(x => new {
                          //     x.BusinessId,
                          //     MyNestedItems = x.MyNestedItems.Select(y => new
                          //    {
                          //      y.SomeString
                          //    },
                          //    X.SomeBoolean
                          // });
                           .Select(DynamicSelectGenerator<Root>(search));

        return await CosmosHelper.QueryAsync(query);
    }

    // Approach sourced from: https://stackoverflow.com/questions/606104/how-to-create-linq-expression-tree-to-select-an-anonymous-type
    private Expression<Func<T, dynamic>> DynamicSelectGenerator<T>(Search search)
    {
        var rootFields = GetRootFieldsToInclude(search.RootFieldsToInclude);

        // input parameter "o"
        var xParameter = Expression.Parameter(typeof(T), "o");

        // create initializers
        var rootFieldBindings = rootFields.Select(o => o.Trim())
            .Select(o =>
            {
                // property "Field1"
                var mi = typeof(Root).GetProperty(o);

                // original value "o.Field1"
                var xOriginal = Expression.Property(xParameter, mi);

                if (o == "MyNestedItems")
                {
                    // When included this fails with 'System.ArgumentException: Incorrect number of arguments supplied for call to method 'System.Collections.Generic.IEnumerable`1[System.Object] Select[MyNested,Object]'
                    //var nestedExpression = GetNestMemberInitExpression(search.MyNestedFieldsToInclude);
                    //var selectMethod = (Expression<Func<Root, IEnumerable<MyNested>>>)(_ => _.MyNestedItems.Select(c => default(MyNested)));
                    //return Expression.Bind(mi, Expression.Call(((MethodCallExpression)selectMethod.Body).Method, nestedExpression));
                }

                // set value "Field1 = o.Field1"
                return Expression.Bind(mi, xOriginal);
            }
        ).ToList();

        // new statement "new Root()"
        var newRoot = Expression.New(typeof(Root));

        // initialization "new Root { Field1 = o.Field1, Field2 = o.Field2 }"
        var newRootExpression = Expression.MemberInit(newRoot, rootFieldBindings);

        // expression "o => new Data { Field1 = o.Field1, Field2 = o.Field2 }"
        return Expression.Lambda<Func<T, dynamic>>(newRootExpression, xParameter);
    }

    private IEnumerable<string> GetRootFieldsToInclude(RootFieldsToInclude rootFieldsToInclude)
    {
        var results = typeof(Root).GetProperties().Select(propertyInfo => propertyInfo.Name).ToList();
        if (rootFieldsToInclude.BusinessId == false)
        {
            results.Remove("BusinessId");
        }

        if (rootFieldsToInclude.MyDateTime == false)
        {
            results.Remove("MyDateTime");
        }

        if (rootFieldsToInclude.SomeBoolean == false)
        {
            results.Remove("SomeBoolean");
        }

        // results.Remove("MyNestedItems");

        return results;
    }

    private MemberInitExpression GetNestMemberInitExpression(MyNestedFieldsToInclude myNestedFieldsToInclude)
    {
        var myNestedFields = GetNestedFieldsToInclude(myNestedFieldsToInclude);

        // input parameter "o"
        var xParameter2 = Expression.Parameter(typeof(MyNested), "n");

        // new statement "new Data()"
        var newNestedItems = Expression.New(typeof(MyNested));

        // create initializers
        var myNestedFieldBindings = myNestedFields.Select(o => o.Trim())
            .Select(o =>
            {
                // property "Field1"
                var mi = typeof(MyNested).GetProperty(o);

                // original value "o.Field1"
                var xOriginal = Expression.Property(xParameter2, mi);

                // set value "Field1 = o.Field1"
                return Expression.Bind(mi, xOriginal);
            }
        ).ToList();

        // initialization "new Data { Field1 = o.Field1, Field2 = o.Field2 }"
        return Expression.MemberInit(newNestedItems, myNestedFieldBindings);
    }

    private IEnumerable<string> GetNestedFieldsToInclude(MyNestedFieldsToInclude myNestedFieldsToInclude)
    {
        var results = typeof(MyNested).GetProperties().Select(propertyInfo => propertyInfo.Name).ToList();

        if (myNestedFieldsToInclude.SomeDate == false)
        {
            results.Remove("SomeDate");
        }

        if (myNestedFieldsToInclude.SomeInteger == false)
        {
            results.Remove("SomeInteger");
        }

        if (myNestedFieldsToInclude.SomeString == false)
        {
            results.Remove("SomeString");
        }

        return results;
    }
}

public class CosmosHelper
{
    public static async Task<IEnumerable<T>> QueryAsync<T>(IQueryable<T> query)
    {
        var docQuery = query.AsDocumentQuery();

        var results = new List<T>();
        while (docQuery.HasMoreResults)
        {
            results.AddRange(await docQuery.ExecuteNextAsync<T>());
        }

        return results;
    }
}

[UPDATE 9/17] 我能够将表达式构建到字段的根级别,并且按预期对CosmosDB执行。我已经更新了上面的代码以反映当前状态。

挑战现在可以正确创建 MyNestedItems 部分。

问题:

  1. 我是走正确的道路还是应该考虑采用其他方法?
  2. 如何从SearchRequest对象构建动态表达式?

谢谢!

0 个答案:

没有答案
相关问题