Entity Framework LINQ Expression для объекта внутри объекта - PullRequest
10 голосов
/ 15 марта 2019

Редактируем этот вопрос в надежде сделать его понятнее.

У нас есть первичная настройка кода структуры объекта Для упрощения примера я упростил два класса, в действительности существует около 10+ классов, похожих на «Запись», где Item является навигационным свойством / внешним ключом.

Класс товара:

public class Item
{
    public int Id { get; set; }
    public int AccountId { get; set; }
    public List<UserItemMapping> UserItemMappings { get; set; }
    public List<GroupItemMapping> GroupItemMappings { get; set; }
}

Класс записи:

public class Record 
{
    public int ItemId { get; set; }
    public Item Item { get; set; }
}

this.User - внедренный пользовательский объект в каждый репозиторий, который содержится в базе данных репозитория. У нас есть хранилище Item со следующим кодом:

var items = this.GetAll()
    .Where(i => i.AccountId == this.User.AccountId);

Я создал следующее выражение в базе данных хранилища, чтобы легко фильтровать его (в надежде на повторное использование). Мы не можем использовать статические методы расширения из-за того, как работает LINQ to Entities (System.NotSupportedException «LINQ to Entities не распознает метод X, и этот метод нельзя преобразовать в выражение хранилища.»).

protected Expression<Func<Item, bool>> ItemIsOnAccount()
{
    return item => item.AccountId == this.User.AccountId;
}

Я решил описанный выше случай, выполнив следующее:

var items = this.GetAll().Where(this.ItemIsOnAccount());

У нас есть дополнительная фильтрация, основанная на разрешениях пользователей в этой учетной записи (опять же, еще один случай, когда я не хочу повторять этот код в каждом нашем репо):

protected Expression<Func<Item, bool>> SubUserCanAccessItem()
{
    return item => this.User.AllowAllItems 
        || item.UserItemMappings.Any(d => d.UserId.Value == this.User.Id) 
        || item.GroupItemMappings.Any(vm => 
            vm.Group.GroupUserMappings
                .Any(um => um.UserId == this.User.Id));
}

Который я могу использовать следующим образом:

    var items = this.GetAll().Where(this.SubUserCanAccessItem());

Однако, что нам также нужно, в хранилище Record есть способ решить следующее:

var records = this.GetAll()
    .Where(i => i.Item.AccountId == this.User.AccountId);

Поскольку Item - это одно навигационное свойство, я не знаю, как применить выражения, которые я создал, к этому объекту.

Я хочу повторно использовать выражение, которое я создал в базе репо, на всех этих других репозиториях, чтобы мой код «на основе разрешений» находился в одном и том же месте, но я не могу просто добавить его, потому что в этом предложении указано условие Where. case имеет выражение >.

Создание интерфейса методом:

Item GetItem();

для него и помещение его в класс Record не работает из-за LINQ для сущностей.

Я также не могу создать базовый абстрактный класс и наследовать от него, потому что могут быть другие объекты, кроме Item, по которым необходимо выполнить фильтрацию. Например, у Записи также может быть «Вещь», которая имеет логику разрешений. Не все объекты требуют фильтрации по «Item» и «Thing», некоторые только по одному, некоторые по другому, некоторые по обоим:

var items = this.GetAll()
    .Where(this.ItemIsOnAccount())
    .Where(this.ThingIsOnAccount());

var itemType2s = this.GetAll().Where(this.ThingIsOnAccount());

var itemType3s = this.GetAll().Where(this.ItemIsOnAccount());

Из-за этого наличие одного родительского класса не будет работать.

Есть ли способ, с помощью которого я могу повторно использовать выражения, которые я уже создал, или, по крайней мере, создать выражение / изменить оригиналы для работы по всей доске в других репозиториях, которые, конечно, возвращают свои собственные объекты в GetAll, но у всех есть свойство навигации к Item? Как мне нужно изменить другие репозитории для работы с ними?

Спасибо

Ответы [ 4 ]

11 голосов
/ 30 марта 2019

Первым шагом для повторного использования выражений является перемещение выражений в общий статический класс.Поскольку в вашем случае они привязаны к User, я бы сделал их User методами расширения (но учтите, что они будут возвращать выражения):

public static partial class UserFilters
{
    public static Expression<Func<Item, bool>> OwnsItem(this User user)
        => item => item.AccountId == user.AccountId;

    public static Expression<Func<Item, bool>> CanAccessItem(this User user)
    {
        if (user.AllowAllItems) return item => true;
        return item => item.UserItemMappings.Any(d => d.UserId.Value == user.Id) ||
            item.GroupItemMappings.Any(vm => vm.Group.GroupUserMappings.Any(um => um.UserId == user.Id));
    }
}

Теперь репозиторий Item будет использовать

var items = this.GetAll().Where(this.User.OwnsItem());

или

var items = this.GetAll().Where(this.User.CanAccessItem());

Для возможности повторного использования для объектов, имеющих ссылку Item, вам понадобится небольшая вспомогательная утилита для составления лямбда-выражений из других лямбда-выражений, аналогичная Преобразование выражения Linq "obj => obj.Prop" в "parent => parent.obj.Prop" .

Возможно реализовать его с Expression.Invoke, но так как не все поставщики запросов поддерживаютдля выражений вызова (EF6 точно не делает, EF Core делает), как обычно, мы будем использовать пользовательский посетитель выражения для замены выражения параметра лямбда другим произвольным выражением:

public static partial class ExpressionUtils
{
    public static Expression ReplaceParameter(this Expression expression, ParameterExpression source, Expression target)
        => new ParameterReplacer { Source = source, Target = target }.Visit(expression);

    class ParameterReplacer : ExpressionVisitor
    {
        public ParameterExpression Source;
        public Expression Target;
        protected override Expression VisitParameter(ParameterExpression node)
            => node == Source ? Target : node;
    }
}

И два составляющихфункции следующие (мне не нравится имя Compose, поэтому иногда я использую имя Map, иногда Select, Bind, Transform и т. д., но функционально они делают то же самое. В этомдело Я использую Apply и ApplyTo, с той лишь разницей, что направление преобразования):

public static partial class ExpressionUtils
{
    public static Expression<Func<TOuter, TResult>> Apply<TOuter, TInner, TResult>(this Expression<Func<TOuter, TInner>> outer, Expression<Func<TInner, TResult>> inner)
        => Expression.Lambda<Func<TOuter, TResult>>(inner.Body.ReplaceParameter(inner.Parameters[0], outer.Body), outer.Parameters);

    public static Expression<Func<TOuter, TResult>> ApplyTo<TOuter, TInner, TResult>(this Expression<Func<TInner, TResult>> inner, Expression<Func<TOuter, TInner>> outer)
        => outer.Apply(inner);
}

(ничего особенного там нет, код предоставлен для полноты)

Теперь вы можетеповторно использовать исходные фильтры, «применяя» их к выражению, которое выбирает свойство Item из другого объекта:

public static partial class UserFilters
{
    public static Expression<Func<T, bool>> Owns<T>(this User user, Expression<Func<T, Item>> item)
        => user.OwnsItem().ApplyTo(item);

    public static Expression<Func<T, bool>> CanAccess<T>(this User user, Expression<Func<T, Item>> item)
        => user.CanAccessItem().ApplyTo(item);
}

и добавляет следующее в репозиторий объектов (в данном случае, Record репозиторий):

static Expression<Func<Record, Item>> RecordItem => entity => entity.Item;

, что позволит вам использовать его

var records = this.GetAll().Where(this.User.Owns(RecordItem));

или

var records = this.GetAll().Where(this.User.CanAccess(RecordItem));

Этого должно быть достаточно для удовлетворения ваших требований.


Вы можете пойти дальше и определить интерфейс, подобный этому

public interface IHasItem
{
    Item Item { get; set; }
}

и позволить сущностям реализовать его

public class Record : IHasItem // <--
{
    // Same as in the example - IHasItem.Item is auto implemented
    // ... 
}

, а затем добавить дополнительные помощники, подобные этому

public static partial class UserFilters
{
    public static Expression<Func<T, Item>> GetItem<T>() where T : class, IHasItem
        => entity => entity.Item;

    public static Expression<Func<T, bool>> OwnsItem<T>(this User user) where T : class, IHasItem
        => user.Owns(GetItem<T>());

    public static Expression<Func<T, bool>> CanAccessItem<T>(this User user) where T : class, IHasItem
        => user.CanAccess(GetItem<T>());
}

, который позволил бы вам опустить выражение RecordItem в репозитории и использовать его вместо

var records = this.GetAll().Where(this.User.OwnsItem<Record>());

или

var records = this.GetAll().Where(this.User.CanAccessItem<Record>());

Не уверен, дает ли он вамлучшая читаемость, но это вариант, и синтаксически он ближе к Item методам.

для Thing и т. д. просто добавьте похожие UserFilters методы.


КакБонус, вы можете пойти еще дальше и добавить обычные PredicateBuilder методы And и Or

public static partial class ExpressionUtils
{
    public static Expression<Func<T, bool>> And<T>(this Expression<Func<T, bool>> left, Expression<Func<T, bool>> right)
        => Expression.Lambda<Func<T, bool>>(Expression.AndAlso(left.Body,
            right.Body.ReplaceParameter(right.Parameters[0], left.Parameters[0])), left.Parameters);

    public static Expression<Func<T, bool>> Or<T>(this Expression<Func<T, bool>> left, Expression<Func<T, bool>> right)
        => Expression.Lambda<Func<T, bool>>(Expression.OrElse(left.Body,
            right.Body.ReplaceParameter(right.Parameters[0], left.Parameters[0])), left.Parameters);
}

, чтобы вы могли использовать что-то вроде этого при необходимости

var items = this.GetAll().Where(this.User.OwnsItem().Or(this.User.CanAccessItem()));

вItem хранилище или

var records = this.GetAll().Where(this.User.OwnsItem<Record>().Or(this.User.CanAccessItem<Record>()));

в Record хранилище.

2 голосов
/ 16 марта 2019

Я не могу точно сказать, может ли это работать в вашем случае, зависит от того, как ваши сущности могут быть настроены, но одну вещь, которую вы можете попробовать, - это иметь такой интерфейс, как IHasItemProperty, с методом GetItem () и иметь сущности, где Вы хотите использовать это реализовать этот интерфейс. Примерно так:

public interface IHasItemProperty {
    Item GetItem();
}

public class Item: IHasItemProperty {

    public Item GetItem() {
       return this;
    }

    public int UserId {get; set;}
}

public class Record: IHasItemProperty {
    public Item item{get;set;}

    public Item GetItem() {
        return this.item;
    }
}

public class Repo
{
    protected Expression<Func<T, bool>> ItemIsOnAccount<T>() where T: IHasItemProperty
    {
        return entity => entity.GetItem().UserId == 5;
    }

}

Я использовал int только для упрощения.

0 голосов
/ 05 апреля 2019

Вы должны быть в состоянии сделать это с помощью .AsQueryable ().

class Account
{
    public IEnumerable<User> Users { get; set; }
    public User SingleUser { get; set; }


    static void Query()
    {
        IQueryable<Account> accounts = new Account[0].AsQueryable();

        Expression<Func<User, bool>> userExpression = x => x.Selected;

        Expression<Func<Account, bool>> accountAndUsersExpression =
            x => x.Users.AsQueryable().Where(userExpression).Any();
        var resultWithUsers = accounts.Where(accountAndUsersExpression);

        Expression<Func<Account, bool>> accountAndSingleUserExpression =
            x => new[] { x.SingleUser }.AsQueryable().Where(userExpression).Any();
        var resultWithSingleUser = accounts.Where(accountAndSingleUserExpression);
    }
}

class User
{
    public bool Selected { get; set; }
}
0 голосов
/ 15 марта 2019

В качестве предиката вы должны использовать только элементы sql (или вашу базу данных). Если вы поместите this.User.AccountId в свою лямбду, которая не существует в базе данных и не может быть проанализирована им, это источник вашего сообщения об ошибке.

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