繁体   English   中英

C#EF6条件属性选择?

[英]C# EF6 conditional property selection?

假设我有代码优先模型:

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中使用这些实体。

据我所知,在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查询中构造

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

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

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 \\从上下文中删除它。

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

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

结果

生成的SQL

生成结果

使用导航属性(计数)

在此输入图像描述

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

例如,给出以下模型

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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM