EF Core TPC映射下如何查询带MobileRefreshToken的用户?
问题描述
我是ASP.NET和EF Core新手,对EF Core中继承和导航属性的工作机制存在误解。我有一个关联用户外键的RefreshToken表,包含两种类型:仅专业用户可使用的WEB类型,以及普通用户和专业用户均可使用的MOBILE类型。
public class RefreshToken { [Key] [StringLength(64)] public string Id { get; set; } [StringLength(100)] public string Token { get; set; } public DateTime ExpiresAt { get; set; } public RefreshTokenType Type { get; set; } // WEB or MOBILE // Foreign key for user [StringLength(64)] public string UserId { get; set; } // Navigation property for user public BaseUserModel User { get; set; } }
BaseUserModel是抽象类,由UserModel和ProUserModel两个具体用户类型继承。采用TPC映射,希望拥有两个独立的用户表。在类中添加了导航属性以便获取所需的刷新令牌,但遇到了问题。
父类代码
public abstract class BaseUserModel { [Key] public string UserId { get; set; } [Column("phone_number")] public string PhoneNumber { get; set; } // navigation properties protected internal virtual ICollection<RefreshToken>? RefreshTokens { get; set; } public RefreshToken? MobileRefreshToken => RefreshTokens?.FirstOrDefault(rt => rt.Type == RefreshTokenType.MobileApp); }
子类代码
public class ProUserModel : BaseUserModel { [Column("email")] public string Email { get; set; } public RefreshToken? WebRefreshToken => RefreshTokens?.FirstOrDefault(rt => rt.Type == RefreshTokenType.WebApp); } public class UserModel : BaseUserModel { [Column("address")] public string Address { get; set; } }
我希望通过user_id查询任意类型的用户,并同时获取其MobileRefreshToken,但尝试多种方法均失败:
尝试的方法及错误
方法一
private async Task<BaseUserModel?> GetUserWithTokenById(string userId) { return await dbContext.BaseUsers.Include(u => u.MobileRefreshToken).SingleOrDefaultAsync(u => u.UserId == userId); }
报错信息:
System.InvalidOperationException: The expression 'u.MobileRefreshToken' is invalid inside an 'Include' operation, since it does not represent a property access: 't => t.MyProperty'. To target navigations declared on derived types, use casting ('t => ((Derived)t).MyProperty') or the 'as' operator ('t => (t as Derived).MyProperty'). Collection navigation access can be filtered by composing Where, OrderBy(Descending), ThenBy(Descending), Skip or Take operations.
方法二
private async Task<BaseUserModel?> GetUserWithTokenById(string userId) { var proUser = await dbContext.ProUsers.Include(u => u.MobileRefreshToken).SingleOrDefaultAsync(u => u.UserId == userId); if (proUser != null) { return proUser; } return await dbContext.Users.Include(u => u.MobileRefreshToken).SingleOrDefaultAsync(u => u.UserId == userId); }
仍出现相同错误。
方法三
var user = await dbContext.Users .SingleOrDefaultAsync(u => u.UserId == userId); if (user != null) { await dbContext.Entry(user) .Reference(u => u.MobileRefreshToken) .LoadAsync(); return user; } // Same for pro user if null
报错信息:
System.InvalidOperationException: The property 'UserModel.MobileRefreshToken' could not be found. Ensure that the property exists and has been included in the model.
解决方案
核心问题是:MobileRefreshToken不是EF Core识别的导航属性,它只是基于RefreshTokens集合计算出的只读属性,EF无法对这类计算属性执行Include或显式加载操作。正确做法是直接操作底层的RefreshTokens导航属性:
方案1:加载完整RefreshTokens集合
直接IncludeRefreshTokens,之后计算属性MobileRefreshToken可正常工作:
private async Task<BaseUserModel?> GetUserWithTokenById(string userId) { // 先查询专业用户 var proUser = await dbContext.ProUsers .Include(u => u.RefreshTokens) .SingleOrDefaultAsync(u => u.UserId == userId); if (proUser != null) return proUser; // 再查询普通用户 return await dbContext.Users .Include(u => u.RefreshTokens) .SingleOrDefaultAsync(u => u.UserId == userId); }
方案2:过滤Include优化性能
如果不想加载所有令牌,可使用EF Core的过滤Include功能,只加载MobileApp类型的令牌:
private async Task<BaseUserModel?> GetUserWithTokenById(string userId) { var proUser = await dbContext.ProUsers .Include(u => u.RefreshTokens.Where(rt => rt.Type == RefreshTokenType.MobileApp)) .SingleOrDefaultAsync(u => u.UserId == userId); if (proUser != null) return proUser; return await dbContext.Users .Include(u => u.RefreshTokens.Where(rt => rt.Type == RefreshTokenType.MobileApp)) .SingleOrDefaultAsync(u => u.UserId == userId); }
补充说明
- 计算属性(如
MobileRefreshToken)不会被EF映射到数据库,只能在内存中基于已加载的导航属性计算; - TPC模式下,抽象类
BaseUserModel不会生成数据库表,必须通过具体的ProUsers和UsersDbSet查询; - 两次查询的方式逻辑清晰,实际性能影响极小,若想避免两次查询,可通过SQL直接查询合并结果,但必要性不高。
内容的提问来源于stack exchange,提问作者Matthias64
相关产品推荐
相关产品推荐

