编辑这个问题是为了让它更清晰。
我们首先设置实体框架代码。为了示例的目的,我简化了两个类,实际上有大约10多个类与'Record'类似,其中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如何工作(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是单个导航属性,所以我不知道如何将我创建的表达式应用于此对象。
我想在repo基础上重用我在所有这些其他repos中创建的表达式,这样我的'基于权限的'代码都在同一个地方,但是我不能简单地把它放进去,因为在这种情况下Where子句是表达式<Func <Record,bool >>。
使用以下方法创建接口:
Item GetItem();
在它上面并将它放在Record类上是不行的,因为LINQ to实体。
我也不能创建一个基本抽象类并从中继承,因为可能有其他对象而不是需要过滤的Item。例如,一个Record也可能有一个具有权限逻辑的“Thing”。并非所有对象都需要通过'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());
由于这个单一的父类不起作用。
有没有一种方法可以重用我已经创建的表达式,或者至少创建一个表达式/修改原始文件,以便在OTHER repos中全面工作,当然在GetAll中返回自己的对象,但所有都有导航属性到项目?我如何修改其他repos以使用这些?
谢谢
表达式可重用性的第一步是将表达式移动到公共静态类。因为在你的情况下,他们绑定到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
引用的实体,您需要一个小的辅助工具来组合其他lambda表达式的lambda表达式,类似于Convert Linq expression "obj => obj.Prop" into "parent => parent.obj.Prop"。
可以使用Expression.Invoke
实现它,但由于并非所有查询提供程序都支持调用表达式(EF6不确定,EF Core会这样做),像往常一样,我们将使用自定义表达式访问者将lambda参数表达式替换为另一个任意表达式表达:
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
存储库中。
我无法确定这是否可以在你的情况下工作,取决于你的实体如何设置,但你可以尝试的一件事是有一个像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只是为了让事情更简单。
您应该可以使用.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; }
}
您应该只为谓词使用sql(或类似数据库)项。如果你将this.User.AccountId放入你的lambda,它在数据库中不存在并且无法被它解析,那就是你的错误消息的来源。