首页 > 解决方案 > C# EF6 条件属性选择?

问题描述

假设我有代码优先模型:

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;
}

但这将通过网络发送includeTitleincludeDescription属性作为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 中使用,除非您明确附加它们。

标签: c#entity-framework

解决方案


据我所知,在 EF 中没有干净的方法可以做到这一点。您可以使用各种丑陋的一些变通方法,以下是一种。只有当您不打算更新\附加\删除返回的实体时,它才会起作用,我认为这对于这个用例来说很好。

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

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 查询中构造

因此,在if是映射实体的类型中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 查询。它确实返回了一个代理类型列表,但这不是问题,因为代理类型继承自您的查询类型。正如我之前所说的那样 - 你不能从上下文中附加\更新\删除它。

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


推荐阅读