在对象的对象中使用Entity Framework LINQ表达式

10

编辑此问题,希望更加清晰明了。

我们有一个Entity Framework Code First设置。为了举例说明,我简化了两个类,实际上还有大约10多个类类似于'Record',其中'Item'是导航属性/外键。

'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存储库:


this.User是注入到每个仓库中的用户对象,包含在仓库的基类中。我们有如下代码的Item仓库:
var items = this.GetAll()
    .Where(i => i.AccountId == this.User.AccountId);

我在仓库基础上创建了以下表达式,以便轻松过滤数据(希望能够重复使用)。由于 LINQ to Entities 的工作原理,我们无法使用静态扩展方法(System.NotSupportedException "LINQ to Entities does not recognize the method X and this method cannot be translated into a store expression.")。

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

然而,在记录存储库中我们还需要一种解决以下问题的方法:

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

由于“Item”是单个导航属性,我不知道如何将我创建的表达式应用到该对象。
我希望重用我在基于所有这些其他存储库创建的存储库中创建的表达式,以便我的“基于权限”的代码都在同一个位置,但是我不能简单地把它扔进去,因为这种情况下的Where子句是Expression<Func<Record,bool>>。
创建一个具有以下方法的接口:
Item GetItem();

因为LINQ to entities的原因,在其上进行操作并将其放在Record类中是行不通的。

我也不能创建一个基础抽象类并从中继承,因为除了需要过滤的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());

由于这个问题只有一个父类是行不通的。

我是否可以重用我已经创建的表达式,或者至少创建一个表达式/修改原始表达式,使其在所有其他仓库中工作,当然,在这些仓库中,它们都返回自己的对象,但都具有导航到项目的属性?我需要如何修改其他仓库才能与这些仓库一起使用?

谢谢


1
你尝试过使用.AsQueryable()吗?这样你就可以随意添加额外的筛选器。 - Senad Meškin
4个回答

14
表达式可重用性的第一步是将这些表达式移动到一个公共的静态类中。由于在您的情况下它们与相关联,因此我会将它们作为扩展方法(但请注意,它们将返回表达式):
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表达式,类似于将Linq表达式“obj => obj.Prop”转换为“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这个名称,因此有时会使用MapSelectBindTransform等名称,但它们在功能上是相同的。在这种情况下,我使用ApplyApplyTo,唯一的区别是转换方向):

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方法AndOr
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}}存储库中。

1
很不幸,我最近非常忙碌,无法检查这个问题。但是,由于你提供了非常复杂和深入的答案,我将授予你奖励,非常感谢。希望我能在接下来的一周左右回来重新评估这个问题。 - Seb
嗨,伊万,这里没有DM函数,但你在这个问题上表现得非常出色,我想知道你是否有时间看一下https://stackoverflow.com/questions/59581816/assign-mapped-object-to-expression-result-in-linq-to-entities - 我不太擅长表达式,你之前的答案帮了我很大的忙,我非常感激。 - Seb
嗨@Seb,这个不是很简单。 值得尝试一些第三方表达式组合帮助库,例如[LinqKit](https://github.com/scottksmith95/LINQKit) Invoke / Expand。 否则,您基本上必须实现类似于那里解释的内容。 或者[NeinLinq](https://github.com/axelheer/nein-linq)。 - Ivan Stoev
1
嗨@Ivan,谢谢你的提示。我会去看看它。我只是想为了可重用性而避免编写2个映射表达式! - Seb
1
谢谢你的建议,Ivan。我使用了你提供的LinqKit来解决这个问题。 - Seb
哇,我真的对表达式上的扩展方法感到惊讶。也许你可以使用它将POCO映射到VM,例如Expression<Func<Item, ItemVM>> AsVM(this Item item) => new ItemVM { prop = item.prop, ...},然后可能是_db.Item.AsVM()。仍然不确定语法的确切形式,但这肯定是一条路。 - Zachary Scott

2

我无法确定这种方法是否适用于你的情况,这取决于你的实体如何设置,但你可以尝试一下以下方法:创建一个像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;
    }

}

我只是为了让事情更简单而使用了整数。

最初的回答

有趣的是,我试图通过继承或接口来完成这个功能(但是通过继承我只能继承一个具有“Item”属性的类,因此无法拥有Item2等,并且无法想出如何使用接口来实现!),谢谢提供这个方法,我会尝试一下的! - Seb
唉,这不是命中注定:System.NotSupportedException LINQ to Entities 不认识 'DataLayer.Models.Item GetItem()' 方法,而且这个方法无法被转换成存储表达式。 - Seb

0
你应该可以使用 .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; }
}

-2

你应该只使用 SQL(或类似的数据库)中的项目作为谓词。如果你将 this.User.AccountId 放入 lambda 中,那么它在数据库中不存在,也无法被解析,这就是你错误信息的来源。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接