C # EF6 условный выбор свойства? - PullRequest
0 голосов
/ 26 апреля 2018

Предположим, у меня есть модель с первым кодом:

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 свойства в качестве параметров SQL для EXEC , и в большинстве случаев запрос будет неэффективным по сравнению с простым безусловным анонимный запрос без этого беспорядка - но запись каждой возможной перестановки анонимной структуры не вариант.

PS : на самом деле существует большой список свойств «включить / исключить», я просто представил два для простоты.

UPDATE:

Вдохновленный ответом @ 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 голосов
/ 03 мая 2018

Использование логического типа для пометки включения поля недопустимо, особенно если у вас длинный список полей. Лучшим подходом является установка необязательных параметров для фильтров и проверка значения перед добавлением его в запрос. Значение необязательного параметра должно быть тщательно выбрано.

Например, с учетом следующей модели

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(string title = "")
{
    var query = ctx.FooBars.AsNoTracking().Where(Id > 123);
    if(!string.isnullorempty(title)
    {
        query = query.where(x=>x.title = title)
    }
    return query;
}

Я понимаю, что выбор необязательного параметра здесь может быть сложным. Я надеюсь, что эта помощь

0 голосов
/ 04 мая 2018

Я использовал System.Linq.Dynamic для этого очень успешно. Вы можете передать строку в качестве оператора выбора в следующем формате: .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;
}

EDIT

Оказывается, 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

0 голосов
/ 26 апреля 2018

Насколько я знаю, в EF нет чистого способа сделать это. Вы можете использовать некоторые обходные пути разного уродства, ниже один. Это будет работать, только если вы не собираетесь обновлять \ присоединять \ удалять возвращенные объекты, что, как я полагаю, подходит для этого варианта использования.

Предположим, мы хотим включить только свойства "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 недопустимо в Select, если SomeType является типом сопоставленной сущности.

Но что, если у нас есть тип, унаследованный от сущности, и используем его?

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

А теперь он работает нормально и генерирует SQL-запрос select только с включенными полями. Он действительно возвращает список типов прокси, но это не проблема, поскольку тип прокси наследуется от вашего типа запроса. Мысль, как я уже говорил, вы не можете прикрепить \ обновить \ удалить из контекста.

Конечно, вы также можете изменить этот метод для исключения, принятия выражений свойств вместо чистых строк и т. Д., Это просто код, подтверждающий идею.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...