C#EF6条件属性选择?

eoc*_*ron 8 c# entity-framework

假设我有代码优先模型:

public class FooBar
{
    [Key]
    public int Id {get;set;}
    [MaxLength(254)]
    public string Title {get;set;}
    public string Description {get;set;}
}
Run Code Online (Sandbox Code Playgroud)

以及检索行的一些数据子集的方法:

public IQueryable<FooBar> GetDataQuery(bool includeTitle, bool includeDescription)
{
    var query = ctx.FooBars.AsNoTracking().Where(Id > 123);
    //how to inlcude/exclude???
    return query;
}
Run Code Online (Sandbox Code Playgroud)

问题是如何在不对匿名类型进行硬编码的情况下使用特定字段构建查询?基本上,我想告诉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;
}
Run Code Online (Sandbox Code Playgroud)

但是这将通过线路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;
    }
}
Run Code Online (Sandbox Code Playgroud)

用法:

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

        }
    }
}
Run Code Online (Sandbox Code Playgroud)

重要的是,在您明确附加它们之前,不能在EF中使用这些实体.

rec*_*ace 4

我非常成功地使用System.Linq.Dynamic来实现此目的。您可以按以下格式传递字符串作为 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;
}
Run Code Online (Sandbox Code Playgroud)

编辑

结果 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;
}
Run Code Online (Sandbox Code Playgroud)

更新

但等等还有更多!

var query = ctx.FooBars
    .Where(f => f.Id > 240)
    .Select(select)
    .ToJson() // using Newtonsoft.JSON, I know, I know, 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;
}
Run Code Online (Sandbox Code Playgroud)

结果

生成的 SQL

生成的结果

具有导航属性(计数)

在此输入图像描述