gpt4 book ai didi

c# - 对象内对象的 Entity Framework LINQ 表达式

转载 作者:可可西里 更新时间:2023-11-01 09:06:04 24 4
gpt4 key购买 nike

编辑这个问题,希望让它更清楚。

我们首先设置了 Entity Framework 代码。为了示例的目的,我简化了两个类,实际上还有大约 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 是一个注入(inject)到每个 repo 中的用户对象,并且包含在 repository base 中。我们有一个包含以下代码的项目存储库:

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

我们根据该帐户中的用户权限进行额外的过滤(同样,我不想在我们拥有的每个 repo 中重复此代码的另一种情况):

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 是 Expression< Func < Record,bool >>.

使用以下方法创建接口(interface):

Item GetItem();

由于 LINQ to entities,将其放在 Record 类中是行不通的。

我不能同时创建一个基本抽象类并从它继承,因为除了 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());

因此,只有一个父类是行不通的。

有没有一种方法可以重用我已经创建的表达式,或者至少创建一个表达式/修改原始表达式以在 OTHER 存储库中全面工作,这些存储库当然会在 GetAll 中返回它们自己的对象,但是所有项目都有导航属性吗?我需要如何修改其他存储库才能使用这些存储库?

谢谢

最佳答案

表达式可重用性的第一步是将表达式移动到一个公共(public)静态类中。因为在你的情况下它们与 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 code>, Bind, Transform 等,但在功能上它们是一样的。在这种情况下,我使用的是 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));

这应该足以满足您的要求。


你可以更进一步,像这样定义一个接口(interface)

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 存储库中。

关于c# - 对象内对象的 Entity Framework LINQ 表达式,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/55187496/

24 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com