Entity Framework Core 5.0 如何将多对多连接的 LINQ 转换为使用 ASP.NET 成员资格的交集表

Posted

技术标签:

【中文标题】Entity Framework Core 5.0 如何将多对多连接的 LINQ 转换为使用 ASP.NET 成员资格的交集表【英文标题】:Entity Framework Core 5.0 How to convert LINQ for many-to-many join to use Intersection table for ASP.NET Membership 【发布时间】:2021-03-22 21:42:45 【问题描述】:

问题: 如何将 LINQ 查询转换为对 INNER JOINS 两个表并具有谓词的子选择执行 LEFT OUTER JOIN?

背景: 我正在使用EFCore Tools 逆向工程 功能从Entity Framework 6 (EF6) 升级到Entity Framework Core 5 (EFCore)。我有一个使用 LINQ 查询 ASP.NET Membership 系统上 AspNet_UsersAspNet_Roles 表之间的多对多关系的查询。该查询通过AspNet_UsersInRoles 交集表抽象出连接。因此,在 EF6 中我有以下 LINQ:

(from r in this.DbContext.aspnet_Users
where r.UserId == dpass.UserId
select r.aspnet_Roles.Select(x => x.RoleName)
).FirstOrDefault();

生成以下 SQL 查询(使用 SQL Server Profiler 检索):

DECLARE @p__linq__0 uniqueidentifier = '<Runtime_GUID>'

SELECT 
    [Limit1].[UserId] AS [UserId], 
    [Join1].[RoleName] AS [RoleName], 
    CASE WHEN ([Join1].[UserId] IS NULL) THEN CAST(NULL AS int) ELSE 1 END AS [C1]
FROM (SELECT TOP (1) [Extent1].[UserId] AS [UserId]
        FROM [dbo].[aspnet_Users] AS [Extent1]
        WHERE [Extent1].[UserId] = @p__linq__0 
        ) AS [Limit1]
LEFT OUTER JOIN (SELECT [Extent2].[UserId] AS [UserId]
                    , [Extent3].[RoleName] AS [RoleName]
                FROM [dbo].[aspnet_UsersInRoles] AS [Extent2]
                INNER JOIN [dbo].[aspnet_Roles] AS [Extent3] 
                    ON [Extent3].[RoleId] = [Extent2].[RoleId] 
                ) AS [Join1] 
    ON [Limit1].[UserId] = [Join1].[UserId]

我已将 LINQ 转换为以下内容:

(from ur in this.DbContext.aspnet_UsersInRoles
 join r in this.DbContext.aspnet_Roles 
    on ur.RoleId equals r.RoleId
  where ur.UserId == dpass.UserId
  join u in this.DbContext.aspnet_Users
    on ur.UserId equals u.UserId
 into UserRolesJoined from UserRoles in UserRolesJoined.DefaultIfEmpty()                              
  select new  ur.UserId, ur.Role.RoleName 
 )

生成以下 SQL 查询(使用 EFCore 的新 .ToQueryString() 方法检索):

DECLARE @__dpass_UserId_1 uniqueIdentifier = '<Runtime_GUID>'
SELECT [a].[UserId], [a2].[RoleName]
FROM [aspnet_UsersInRoles] AS [a]
INNER JOIN [aspnet_Roles] AS [a0] ON [a].[RoleId] = [a0].[RoleId]
LEFT JOIN [aspnet_Users] AS [a1] ON [a].[UserId] = [a1].[UserId]
INNER JOIN [aspnet_Roles] AS [a2] ON [a].[RoleId] = [a2].[RoleId]
WHERE [a].[UserId] = @__dpass_UserId_1

EFCore 的 SQL SELECT 语句 不同于 EF6 的 SQL SELECT 语句;但是,SQL Select result 是相同的。这是重新编写 LINQ 的正确方法,还是我弄错了?非常感谢任何帮助。

更新

我运行逆向工程工具后,原来的 LINQ 命令出现以下错误:

    CS1061 'aspnet_Users' 不包含'aspnet_Roles' 的定义,并且找不到接受'aspnet_Users' 类型的第一个参数的可访问扩展方法'aspnet_Roles'(您是否缺少 using 指令或程序集引用?)李>

当我将public IEnumerable&lt;object&gt; aspnet_Roles; 添加到aspnet_users 时,我收到以下错误。

    CS1061 'object' 不包含 'RoleName' 的定义,并且找不到接受第一个类型为 'object' 的参数的可访问扩展方法 'RoleName'(您是否缺少 using 指令或程序集引用?)李>

以下是 Microsoft 创建的 ASP.NET 会员系统中的相关表格。有关完整架构的参考,请参阅:https://docs.microsoft.com/en-us/aspnet/web-forms/overview/older-versions-security/membership/creating-the-membership-schema-in-sql-server-cs

aspnet_UserInRoles

[Index(nameof(RoleId), Name = "aspnet_UsersInRoles_index")]
public partial class aspnet_UsersInRoles

    [Key]
    public Guid UserId  get; set; 
    [Key]
    public Guid RoleId  get; set; 

    [ForeignKey(nameof(RoleId))]
    [InverseProperty(nameof(aspnet_Roles.aspnet_UsersInRoles))]
    public virtual aspnet_Roles Role  get; set; 
    [ForeignKey(nameof(UserId))]
    [InverseProperty(nameof(aspnet_Users.aspnet_UsersInRoles))]
    public virtual aspnet_Users User  get; set; 

aspnet_Roles

public partial class aspnet_Roles

    public aspnet_Roles()
    
        aspnet_UsersInRoles = new HashSet<aspnet_UsersInRoles>();
    

    public Guid ApplicationId  get; set; 
    [Key]
    public Guid RoleId  get; set; 
    [Required]
    [StringLength(256)]
    public string RoleName  get; set; 
    [Required]
    [StringLength(256)]
    public string LoweredRoleName  get; set; 
    [StringLength(256)]
    public string Description  get; set; 

    [ForeignKey(nameof(ApplicationId))]
    [InverseProperty(nameof(aspnet_Applications.aspnet_Roles))]
    public virtual aspnet_Applications Application  get; set; 
    [InverseProperty("Role")]
    public virtual ICollection<aspnet_UsersInRoles> aspnet_UsersInRoles  get; set; 

aspnet_Users

[Index(nameof(ApplicationId), nameof(LastActivityDate), Name = "aspnet_Users_Index2")]
public partial class aspnet_Users

    public aspnet_Users()
    
        Users = new HashSet<Users>();
        aspnet_PersonalizationPerUser = new HashSet<aspnet_PersonalizationPerUser>();
        aspnet_UsersInRoles = new HashSet<aspnet_UsersInRoles>();
    

    public Guid ApplicationId  get; set; 
    [Key]
    public Guid UserId  get; set; 
    [Required]
    [StringLength(256)]
    public string UserName  get; set; 
    [Required]
    [StringLength(256)]
    public string LoweredUserName  get; set; 
    [StringLength(16)]
    public string MobileAlias  get; set; 
    public bool IsAnonymous  get; set; 
    [Column(TypeName = "datetime")]
    public DateTime LastActivityDate  get; set; 

    [ForeignKey(nameof(ApplicationId))]
    [InverseProperty(nameof(aspnet_Applications.aspnet_Users))]
    public virtual aspnet_Applications Application  get; set; 
    [InverseProperty("User")]
    public virtual aspnet_Membership aspnet_Membership  get; set; 
    [InverseProperty("User")]
    public virtual aspnet_Profile aspnet_Profile  get; set; 
    [InverseProperty("aspUser")]
    public virtual ICollection<Users> Users  get; set; 
    [InverseProperty("User")]
    public virtual ICollection<aspnet_PersonalizationPerUser> aspnet_PersonalizationPerUser  get; set; 
    [InverseProperty("User")]
    public virtual ICollection<aspnet_UsersInRoles> aspnet_UsersInRoles  get; set; 

【问题讨论】:

首先,两个 LINQ 查询是不等价的 - 原始查询从主表 aspnet_Users 开始,而转换后的查询从连接表 aspnet_UsersInRoles 开始。这立即产生了差异。其次,为什么要重写原始查询? EFC 5 支持多对多与隐式连接表的方式与 EF6 完全相同,因此只需放置相同的导航属性,删除显式连接实体(但配置它,因为它使用非常规名称)将允许您只使用您的现有查询。 @IvanStoev 我提供了我的问题的更新。我想如果 EFCore 支持多对多关系,那么我不会收到这些错误。所以,我尝试重新编写 LINQ。你知道我该如何解决这个问题吗? @IvanStoev:除非 EFC5 添加它(我没听说过),否则 EF Core 不支持隐式多对多。您需要明确地执行此操作,即假装它是与您自己创建的交叉表的两个一对多关系。 link:“像这样的链接表 [...] EF6.x 在您定义多对多关系时为您创建了此表,但采用更精简方法的 EF Core 并没有——你需要这样做。” @Flater 这是What's New in EF Core 5.0 中最重要的功能。文档here. 【参考方案1】:

如果我是你,我会这样写这个查询:

var result = context.UserRoles
                    .Where(x => x.UserId == ID_TO_SEARCH)
                    .Join(
                        context.Roles,
                        ur => ur.RoleId,
                        r => r.Id,
                        (ur, role) => new
                        
                            ur,
                            role
                        
                    )
                    .Select(x => x.role.Name)
                    .FirstOrDefault();

这产生的查询,对我来说,完全没问题,更优雅:

SELECT TOP(1) [a0].[Name]
FROM [AspNetUserRoles] AS [a]
INNER JOIN [AspNetRoles] AS [a0] ON [a].[RoleId] = [a0].[Id]
WHERE [a].[UserId] = N''

更新:

如果我正确理解了 cmets 中询问的内容,则此查询将选择角色名称 LIKE LEFT JOIN:

var rolesQuery = context.UserRoles
                         .Join(
                            context.Roles,
                            ur => ur.RoleId,
                            r => r.Id,
                            (ur, r) => new
                            
                                ur,
                                r
                            
                        );
                    
    var result = context.Users
                        .Where(x => x.Id == "")
                        .Select(u => new
                        
                           Name = u.UserName,
                           Role = rolesQuery
                               .Where(sub=> sub.ur.UserId == u.Id)
                               .Select(sub=> sub.r.Name)
                               .FirstOrDefault()
                        )
                        .FirstOrDefault();

这会导致这条 SQL 语句:

      SELECT TOP(1) [a1].[UserName] AS [Name], (
          SELECT TOP(1) [a0].[Name]
          FROM [AspNetUserRoles] AS [a]
          INNER JOIN [AspNetRoles] AS [a0] ON [a].[RoleId] = [a0].[Id]
          WHERE [a].[UserId] = [a1].[Id]) AS [Role]
      FROM [AspNetUsers] AS [a1]
      WHERE [a1].[Id] = N''

如您所见,没有 LEFT JOIN,但 sub-select 将以与 LEFT JOIN 类似的方式返回数据。不幸的是,基于 lambda 的查询不支持完整的 LEFT JOIN,编写真正的 LEFT JOIN 的唯一选项可以丰富类似于 SQL 的IQueryable

我在 EF core 5 lib 中看到了一个名为 LeftJoin() 的方法,但它抛出了 NotImplementedException。我认为它会在以后发布的东西

【讨论】:

@dantety89 感谢您提供此查询的 lambda 版本。作为学习练习,我如何将UserId 列添加到输出中?正如@SvyatoslavDanyliv 提到的,我不需要输出中的UserId,但我认为知道如何添加它是值得的。 替换为 .Select(x=> new x.ur.UserId, x.role.Name ) 谢谢。但是,在 OP 中,带有 INNER JOIN 的子选择通过 RoleId 连接,而带有 LEFT OUTER JOIN 的主选择连接 UserId。您能否修改查询以显示如何执行此操作?【参考方案2】:

如果您只需要特定用户的角色名称,则应简化查询:

var query = 
   from r in this.DbContext.aspnet_Users
   where r.UserId == dpass.UserId
   from ro in r.aspnet_Roles
   select ro.RoleName;

【讨论】:

我需要UserIdRoleName。但是,我遇到了与今天早上更新我的问题时提供的相同的错误。关于如何解决它的任何想法? UserId 是已知的,您不必选择它。异常表明导航属性aspnet_Roles 有问题。很难说发生了什么。

以上是关于Entity Framework Core 5.0 如何将多对多连接的 LINQ 转换为使用 ASP.NET 成员资格的交集表的主要内容,如果未能解决你的问题,请参考以下文章

Entity Framework 5.0 Code First全面学习

转:Entity Framework 5.0 Code First全面学习

Entity Framework 5.0 PostgreSQL (Npgsql) 默认连接工厂

Entity Framework 5.0系列之自动生成Code First代码

Entity Framework Core 6.0 预览4 性能改进

Entity Framework Core快速开始