Первым шагом для повторного использования выражений является перемещение выражений в общий статический класс.Поскольку в вашем случае они привязаны к 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
хранилище.