在对象

时间:2019-03-15 17:02:21

标签: c# entity-framework linq repository expression

编辑此问题,希望更清楚。

我们首先设置了实体框架代码。出于示例目的,我简化了两个类,实际上,与“记录”相似,大约有十多个类,其中“项”是导航属性/外键。

物品类别:

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

我在存储库的基础上创建了follow表达式,以轻松地对其进行过滤(以希望再次使用)。由于LINQ to实体的工作方式,我们不能使用静态扩展方法(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子句,我不能简单地将其放入情况是Expression >。

使用以下方法创建界面

Item GetItem();

在其上并将其放在Record类上是由于对实体的LINQ无效。

我也不能创建基础抽象类并从其继承,因为除Item之外,可能还有其他对象需要过滤。例如,记录中可能还包含具有权限逻辑的“事物”。并非所有对象都需要使用“项”和“事物”进行过滤,某些对象仅需一个,某些对象又需要,两者都可以:

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中返回自己的对象,但是都具有Item的导航属性?我需要如何修改其他存储库才能使用这些存储库?

谢谢

4 个答案:

答案 0 :(得分:11)

表达式可重用性的第一步是将表达式移到公共静态类。由于您的情况将它们绑定到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,有时使用SelectBindTransform等,但在功能上它们是相同的。在这种情况下,我使用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 :(得分: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;
    }

}

我使用int只是为了简化事情。

答案 2 :(得分: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; }
}

答案 3 :(得分:-1)

您只能将sql(或类似数据库的数据)项用作谓词。如果将this.User.AccountId放入lambda中,则该值在数据库中不存在并且无法解析,这就是错误消息的来源。