C#EF6条件属性选择?

时间:2018-04-26 08:42:32

标签: c# entity-framework

假设我有代码优先模型:

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 :实际上有大量的&#34;包含/排除&#34;属性,我只是为了简单而提出了两个。

更新

@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中使用,直到您明确附加它们为止。

3 个答案:

答案 0 :(得分:4)

据我所知,在EF中没有干净的方法。你可以使用一些各种丑陋的解决方法,下面是一个。它只有在你不打算更新\ attach \ delete返回的实体时才会起作用,我认为这个用例很好。

假设我们只想包含属性“ID”和“代码”。我们需要构造这种形式的表达式:

fooBarsQuery.Select(x => new FooBar {ID = x.ID, Code = x.Code))

我们可以像这样手动完成:

public static IQueryable<T> IncludeOnly<T>(this IQueryable<T> query, params string[] properties) {
    var arg = Expression.Parameter(typeof(T), "x");
    var bindings = new List<MemberBinding>();

    foreach (var propName in properties) {
        var prop = typeof(T).GetProperty(propName);
        bindings.Add(Expression.Bind(prop, Expression.Property(arg, prop)));
    }
    // our select, x => new T {Prop1 = x.Prop1, Prop2 = x.Prop2 ...}
    var select = Expression.Lambda<Func<T, T>>(Expression.MemberInit(Expression.New(typeof(T)), bindings), arg);
    return query.Select(select);
}

但如果我们真的尝试过:

// some test entity I use
var t = ctx.Errors.IncludeOnly("ErrorID", "ErrorCode", "Duration").Take(10).ToList();

它将失败,异常

  

无法构建实体或复杂类型   在LINQ to Entities查询中

因此,如果new SomeType是映射实体的类型,则SelectSomeType中是非法的。

但是如果我们有一个从实体继承的类型并使用它呢?

public class SomeTypeProxy : SomeType {}

那么它会起作用。所以我们需要在某处获得这样的代理类型。使用内置工具在运行时很容易生成它,因为我们所需要的只是继承某种类型而且都是。

考虑到这一点,我们的方法变为:

static class Extensions {
    private static ModuleBuilder _moduleBuilder;
    private static readonly Dictionary<Type, Type> _proxies = new Dictionary<Type, Type>();

    static Type GetProxyType<T>() {
        lock (typeof(Extensions)) {
            if (_proxies.ContainsKey(typeof(T)))
                return _proxies[typeof(T)];

            if (_moduleBuilder == null) {
                var asmBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(
                    new AssemblyName("ExcludeProxies"), AssemblyBuilderAccess.Run);

                _moduleBuilder = asmBuilder.DefineDynamicModule(
                    asmBuilder.GetName().Name, false);
            }

            // Create a proxy type
            TypeBuilder typeBuilder = _moduleBuilder.DefineType(typeof(T).Name + "Proxy",
                TypeAttributes.Public |
                TypeAttributes.Class,
                typeof(T));

            var type = typeBuilder.CreateType();
            // cache it
            _proxies.Add(typeof(T), type);
            return type;
        }
    }

    public static IQueryable<T> IncludeOnly<T>(this IQueryable<T> query, params string[] properties) {
        var arg = Expression.Parameter(typeof(T), "x");
        var bindings = new List<MemberBinding>();

        foreach (var propName in properties) {
            var prop = typeof(T).GetProperty(propName);
            bindings.Add(Expression.Bind(prop, Expression.Property(arg, prop)));
        }

        // modified select, (T x) => new TProxy {Prop1 = x.Prop1, Prop2 = x.Prop2 ...}
        var select = Expression.Lambda<Func<T, T>>(Expression.MemberInit(Expression.New(GetProxyType<T>()), bindings), arg);
        return query.Select(select);
    }
}

现在它工作正常并生成只包含字段的select sql查询。它确实返回了一个代理类型列表,但这不是问题,因为代理类型继承自您的查询类型。按照我之前的说法 - 你不能附加\ update \从上下文中删除它。

当然你也可以修改这个方法来排除,接受属性表达式而不是纯字符串等等,这只是想法证明代码。

答案 1 :(得分:3)

我非常成功地使用了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;
}

修改

变成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, 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;
}

<强>结果

Generated SQL

Generated results

With Navigation properties (Counting)

enter image description here

答案 2 :(得分:-1)

使用布尔类型标记包含字段是不可持续的,尤其是当您有一长串字段时。更好的方法是为过滤器设置可选参数,并在将值添加到查询之前检查该值。应谨慎选择可选参数的值。

例如,给定以下模型

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

知道Title字段不能为空。我可以构建我的查询,如

public IQueryable<FooBar> GetDataQuery(string title = "")
{
    var query = ctx.FooBars.AsNoTracking().Where(Id > 123);
    if(!string.isnullorempty(title)
    {
        query = query.where(x=>x.title = title)
    }
    return query;
}

我知道在这里选择可选参数可能很棘手。 我希望这有帮助