Linq to Entities - 是否可以在对数据库的单个查询中实现这一点?

Linq to Entities - is it possible to achieve this in single query to DB?

我有多个实体对象,我想通过使用存储在 SQL 服务器数据库中的自定义访问列表 (ACL) 来保护它们。我所有的 "securables" 实现了一个 ISecurable 接口:

public interface ISecurable
{
    int ACLId { get; }
    ACL ACL { get; }
}

AccessList (ACL) 实体如下所示:

public class ACL
{
    public int Id { get; set; }
    public virtual ICollection<ACE> ACEntries { get; set; }
}

...并且每个 ACL 都有多个 ACE 条目:

public class ACE
{
    public int Id { get; set; }

    public int ACLId { get; set; }
    public virtual ACL ACL { get; set; }

    public int PrincipalId { get; set; }
    public virtual Principal Principal { get; set; }

    public int AllowedActions { get; set; }  // flags
}

Actions 可以授予 UsersWorkgroups

Principal是一个抽象class,UserWorkgroup都继承自它:

public abstract class Principal
{
    public int Id { get; set; }
    public string DisplayName { get; set; }

    public virtual ICollection<ACE> ACEntries { get; set; }
}

public class User : Principal
{
    public string Email { get; set; }

    public virtual ICollection<Workgroup> Workgroups { get; set; }
}

public class Workgroup : Principal
{
    public virtual ICollection<User> Users { get; set; }
}

UserWorkgroup显然是多对多的关系。

我的 DbContext 是这样的:

public class MyDbContext : DbContext
{   
    public DbSet<User> Users { get; set; }
    public DbSet<Workgroup> Workgroups { get; set; }
    public DbSet<ACL> ACLs { get; set; }
    public DbSet<ACE> ACEntries { get; set; }
    public DbSet<SecurableClass> SecurableClassItems { get; set; }
}

最后,我的问题:我想编写扩展方法,根据用户和需要的行动。我想对数据库进行单一查询:

    public static IQueryable<ISecurable> FilterByACL(this IQueryable<ISecurable> securables, User user, int requiredAction)
    {
        var userId = user.Id;

        return securables.Where(s =>
            s.ACL.ACEntries.Any(e =>
                (e.PrincipalId == userId || user.Workgroups.Select(w => w.Id).Contains(userId)) &&
                (e.AllowedActions & requiredAction) == requiredAction));

    }

由于错误,这不起作用:

Unable to create a constant value of type 'Test.Entities.Workgroup'. Only primitive types or enumeration types are supported in this context

尽管我 select 只有来自工作组的 ID:

user.Workgroups.Select(w => w.Id)

有什么办法可以处理这种情况吗?抱歉,问题很长,但现有的简化代码可能最能解释我的意图。

更新

经过@vittore的建议,我的扩展方法可能是这样的:

public static IQueryable<ISecurable> FilterByACL2(this IQueryable<ISecurable> securables, User user, int requiredAction, MyDbContext db)
{
    var userId = user.Id;

    var workgroups = db.Entry(user).Collection(u => u.Workgroups).Query();

    return securables.Where(s =>
        s.ACL.ACEntries.Any(e =>
            (e.PrincipalId == userId || workgroups.Select(w => w.Id).Contains(e.PrincipalId)) &&
            (e.AllowedActions & requiredAction) == requiredAction));

}

.. 但我必须引用 'MyDbContext' 作为附加参数 ?

另一方面,如果我编写 SQL 函数 (TVF),它将 return 所有基于 'UserId' 和 'RequiredAction' 的允许的 ACLID:

CREATE function [dbo].[AllowableACLs]
(
    @UserId int,
    @RequiredAction int
)
returns table
as
return 
    select
        ace.ACLId
    from
        dbo.ACEntries ace
    where
        (@UserId = ace.PrincipalId or @UserId in (select wu.User_Id from dbo.WorkgroupUsers wu where ace.PrincipalId = wu.Workgroup_Id))
        and
        ((ace.AllowedActions & @RequiredAction) = @RequiredAction)

.. 理论上我可以从我的 ISecurable 实体中制作 'inner join' 或 'exist (select 1 from dbo.AllowableACLs(@UserId, @RequiredAction)'。

有什么方法可以让代码优先工作吗?

user.Workgroups 是由一系列 c# 对象组成的局部变量。没有将这些对象翻译成 SQL。 EF 需要这种翻译,因为该变量在表达式中使用。

但避免异常很简单:首先创建原始 ID 值列表:

var workGroupIds = user.Workgroups.Select(w => w.Id).ToArray();

并在查询中:

e.PrincipalId == userId || workGroupIds.Contains(userId)

经过你的评论看来你也可以做...

return securables.Where(s =>
    s.ACL.ACEntries.Any(e =>
        (e.PrincipalId == userId 
             || e.Principal.Workgroups.Select(w => w.Id).Contains(userId))
    && (e.AllowedActions & requiredAction) == requiredAction));

...这将在一个查询中执行所有操作。

您需要将 "user.Workgroups.Select..." 移出表达式。 EF 尝试将该表达式转换为 sql 并失败,因为 "user" 是局部变量。因此,这应该有助于修复错误:

public static IQueryable<ISecurable> FilterByACL(this IQueryable<ISecurable> securables,
    User user, int requiredAction)
{
    var userId = user.Id;
    var groupIds = user.Workgroups.Select(w => w.Id).ToArray();
    return securables.Where(s =>
        s.ACL.ACEntries.Any(e =>
            (e.PrincipalId == userId || groupIds.Contains(userId)) &&
            (e.AllowedActions & requiredAction) == requiredAction));

}

请注意,为确保这只是一个查询,您需要在从数据库中检索用户时加载用户的工作组 属性。否则,如果您启用了延迟加载,这将首先为给定用户加载工作组,所以 2 个查询。

为了在一次旅行中获取该信息,您有多种选择。

  1. 最明显的方法是使用连接和 linq 语法:

接近于:

var accessbleSecurables = from s in securables
                          from u in ctx.Users
                          from g in u.Workgroups
                          from e in s.ACL.entries
                          where u.id == userId ...
                          select s

因此它将在查询安全对象时使用数据库中的用户和她的组

  1. 使用 CreateSourceQuery
  2. 将您的工作组转换为查询

这样您将让 EF 知道它需要在数据库级别执行您的包含。

更新:CreateSourceQuery 是 EF3 - EF4 天的旧名称。现在你可以使用 DbCollectionEntry.Query() 方法,像这样:

  var wgQuery = ctx.Entry(user).Collection(u=>u.Workgroups).Query();

然后您就可以在查询中使用它,但您需要引用 EF 上下文才能执行此操作。

  1. 创建将执行相同 sql 查询的存储过程

更新 2:创建存储过程的独特优点之一是能够为其映射多个 return 结果集 (MARS)。例如,您可以通过这种方式在一次数据库访问中查询用户的所有工作组和所有安全对象。