背景和问题
我有一个网站公开了自定义报告生成功能,用户可以在其中动态选择他们想要包含在报告中的字段。 这些字段映射到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 部分。
问题:
谢谢!