假设我有代码优先模型:
public class FooBar { [Key] public int Id {get;set;} [MaxLength(254)] public string Title {get;set;} public string Description {get;set;} }
以及检索行的一些数据子集的方法:
public IQueryable<FooBar> GetDataQuery(bool includeTitle,bool includeDescription) { var query = ctx.FooBars.AsNoTracking().Where(Id > 123); //how to inlcude/exclude??? return query; }
问题是如何使用特定字段构建查询而无需对匿名类型进行硬编码?基本上,我想告诉SQL查询构建器使用指定的字段构建查询,而不在客户端上进行后期过滤.因此,如果我排除描述 – 它将不会通过电汇发送.
此外,有这样的经验:
public IQueryable<FooBar> GetDataQuery(bool includeTitle,bool includeDescription) { var query = ctx.FooBars.AsNoTracking().Where(Id > 123); query = query.Select(x=> new { Id = x.Id Title = includeTitle ? x.Title : null,Description = includeDescription ? x.Description : null,}) .MapBackToFooBaRSSomehow();//this will fail,I know,do not want to write boilerplate to hack this out,just imagine return type will be correctly retrieved return query; }
但是这将通过有线的includeTitle,includeDescription属性作为EXEC的sql参数发送,并且在大多数情况下查询将与没有这种混乱的简单非条件匿名查询相比效率低 – 但是编写匿名结构的每个可能的排列都不是一种选择.
PS:实际上有大量的“包含/排除”属性,我只是为了简单而提出了两个.
更新:
受@reckface答案的启发,我为那些希望在查询结束时实现流畅的执行和映射到实体的人编写了扩展:
public static class CustomsqlMapperExtension { public sealed class SpecBatch<T> { internal readonly List<Expression<Func<T,object>>> Items = new List<Expression<Func<T,object>>>(); internal SpecBatch() { } public SpecBatch<T> Property(Expression<Func<T,object>> selector,bool include = true) { if (include) { Items.Add(selector); } return this; } } public static List<T> WithCustom<T>(this IQueryable<T> source,Action<SpecBatch<T>> configurator) { if (source == null) return null; var batch = new SpecBatch<T>(); configurator(batch); if (!batch.Items.Any()) throw new ArgumentException("Nothing selected from query properties",nameof(configurator)); LambdaExpression lambda = CreateSelector(batch); var rawQuery = source.Provider.CreateQuery( Expression.Call( typeof(Queryable),nameof(Queryable.Select),new[] { source.ElementType,lambda.Body.Type },source.Expression,Expression.Quote(lambda)) ); return rawQuery.ToListAsync().Result.ForceCast<T>().ToList(); } private static IEnumerable<T> ForceCast<T>(this IEnumerable<object> enumer) { return enumer.Select(x=> Activator.CreateInstance(typeof(T)).ShallowAssign(x)).Cast<T>(); } private static object ShallowAssign(this object target,object source) { if (target == null || source == null) throw new ArgumentNullException(); var type = target.GetType(); var data = source.GetType().GetProperties() .Select(e => new { e.Name,Value = e.GetValue(source) }); foreach (var property in data) { type.GetProperty(property.Name).SetValue(target,property.Value); } return target; } private static LambdaExpression CreateSelector<T>(SpecBatch<T> batch) { var input = "new(" + string.Join(",",batch.Items.Select(GetMemberName<T>)) + ")"; return System.Linq.Dynamic.DynamicExpression.ParseLambda(typeof(T),null,input); } private static string GetMemberName<T>(Expression<Func<T,object>> expr) { var body = expr.Body; if (body.NodeType == ExpressionType.Convert) { body = ((UnaryExpression) body).Operand; } var memberExpr = body as MemberExpression; var propInfo = memberExpr.Member as PropertyInfo; return propInfo.Name; } }
用法:
public class Topic { public long Id { get; set; } public string Title { get; set; } public string Body { get; set; } public string Author { get; set; } public byte[] logo { get; set; } public bool IsDeleted { get; set; } } public class MyContext : DbContext { public DbSet<Topic> Topics { get; set; } } class Program { static void Main(string[] args) { using (var ctx = new MyContext()) { ctx.Database.Log = Console.WriteLine; var query = (ctx.Topics ?? Enumerable.Empty<Topic>()).AsQueryable(); query = query.Where(x => x.Title != null); var result = query.WithCustom( cfg => cfg //include whitelist config .Property(x => x.Author,true) //include .Property(x => x.Title,false) //exclude .Property(x=> x.Id,true)); //include } } }
重要的是,在您明确附加它们之前,不能在EF中使用这些实体.
解决方法
我非常成功地使用了
System.Linq.Dynamic.你可以传递一个字符串
作为以下格式的select语句:.Select(“new(Title,Description)”)
作为以下格式的select语句:.Select(“new(Title,Description)”)
所以你的例子将成为:
// ensure you import the System.Linq.Dynamic namespace public IQueryable<FooBar> GetDataQuery(bool includeTitle,bool includeDescription) { // build a list of columns,at least 1 must be selected,so maybe include an Id var columns = new List<string>(){nameof(FooBar.Id)}; if (includeTitle) columns.Add(nameof(FooBar.Title)); if (includeDescription) columns.Add(nameof(FooBar.Description)); // join said columns var select = $"new({string.Join(",columns)})"; var query = ctx.FooBars.AsQueryable() .Where(f => f.Id > 240) .Select(select) .OfType<FooBar>(); return query; }
编辑
变成OfType()可能在这里不起作用.如果是这样的话,这是一个穷人的扩展方法:
// not ideal,but it fits your constraints var query = ctx.FooBars.AsQueryable() .Where(f => f.Id > 240) .Select(select) .ToListAsync().Result .Select(r => new FooBar().Fill(r)); public static T Fill<T>(this T item,object element) { var type = typeof(T); var data = element.GetType().GetProperties() .Select(e => new { e.Name,Value = e.GetValue(element) }); foreach (var property in data) { type.GetProperty(property.Name).SetValue(item,property.Value); } return item; }
更新
但等等还有更多!
var query = ctx.FooBars .Where(f => f.Id > 240) .Select(select) .ToJson() // using Newtonsoft.JSON,awful. .FromJson<IEnumerable<FooBar>>() .AsQueryable(); // this is no longer valid or necessary return query; public static T FromJson<T>(this string json) { var serializer = new JsonSerializer(); using (var sr = new StringReader(json)) using (var jr = new JsonTextReader(sr)) { var result = serializer.Deserialize<T>(jr); return result; } } public static string ToJson(this object data) { if (data == null) return null; var json = JsonConvert.SerializeObject(data,Newtonsoft.Json.Formatting.Indented); return json; }
结果