采用标记接口解决泛型Microsoft Identity组件在Razor Pages与控制器中的依赖注入问题的可行性咨询
泛型Identity组件在ASP.NET Core复用中的依赖注入问题与标记接口方案咨询
背景:泛型Identity组件实现
我基于Microsoft Identity重写了默认的IdentityUser与UserStore,并封装在类库中,让不同项目可以根据需求指定不同的键类型(如Guid、int、string),实现代码如下:
public class ApplicationUser<TIdentityKey, TClientKey> : IdentityUser<TIdentityKey>, IApplicationUser<TIdentityKey, TClientKey> where TIdentityKey : IEquatable<TIdentityKey> where TClientKey : IEquatable<TClientKey> { public TClientKey TenantId { get; set; } } public class ApplicationUserStore<TUser, TRole, TIdentityKey, TClientKey> : UserStore<TUser, TRole, IdentityServerDbContext<TIdentityKey, TClientKey>, TIdentityKey> where TUser : ApplicationUser<TIdentityKey, TClientKey> where TRole : ApplicationRole<TIdentityKey> where TIdentityKey : IEquatable<TIdentityKey> where TClientKey : IEquatable<TClientKey> { private readonly IdentityServerDbContext<TIdentityKey, TClientKey> _context; private readonly ITenantService<TIdentityKey, TClientKey> _tenantService; public ApplicationUserStore(IdentityServerDbContext<TIdentityKey, TClientKey> context, ITenantService<TIdentityKey, TClientKey> tenantService) : base(context) { _context = context; _tenantService = tenantService; } public async override Task<IdentityResult> CreateAsync(TUser user, CancellationToken cancellationToken = default) { user.TenantId = await GetTenantId(); bool combinationExists = await _context.Users .AnyAsync(x => x.UserName == user.UserName && x.Email == user.Email && x.TenantId.Equals(user.TenantId)); if (combinationExists) { var IdentityError = new IdentityError { Description = "The specified username and email are already registered" }; return IdentityResult.Failed(IdentityError); } return await base.CreateAsync(user); } private async Task<TClientKey> GetTenantId() { var tenant = await _tenantService.GetCurrentTenant(); if (tenant == null) return default(TClientKey); else return tenant.Id; } }
遇到的实际问题
在项目中使用这些泛型组件时,遇到了多处依赖注入与复用的障碍:
1. Razor Pages无法使用泛型PageModel
Identity相关页面(如确认邮箱页)需要注入UserManager,但Razor Pages不支持泛型PageModel,定义泛型PageModel后在Razor视图中会出现语法错误:
public class ConfirmEmailModel<TIdentityKey,TClientKey> : PageModel where TIdentityKey:IEquatable<TIdentityKey> where TClientKey:IEquatable<TClientKey> { private readonly UserManager<ApplicationUser<TIdentityKey,TClientKey>> _userManager; public ConfirmEmailModel (UserManager<ApplicationUser<TIdentityKey,TClientKey>> userManager) { _userManager = userManager; } [TempData] public virtual string StatusMessage { get; set; } public virtual async Task<IActionResult> OnGetAsync(string userId, string code) { if (userId == null || code == null) { return RedirectToPage("/Index"); } var user = await _userManager.FindByIdAsync(userId); if (user == null) { return NotFound($"Unable to load user with ID '{userId}'."); } code = Encoding.UTF8.GetString(WebEncoders.Base64UrlDecode(code)); var result = await _userManager.ConfirmEmailAsync(user, code); StatusMessage = result.Succeeded ? "Thank you for confirming your email." : "Error confirming your email."; return Page(); } }
Razor视图中的错误示例:
@page @model ConfirmEmailModel<T>// SYNTAX ERROR @{ ViewData["Title"] = "Confirm email"; } <h1>@ViewData["Title"]</h1>
2. 控制器中需定义泛型参数才能注入泛型服务
使用SignInManager或自定义UserStore时,必须为控制器定义泛型参数,导致控制器复用性下降:
public class BaseUserInfoController<TIdentityKey,TClientKey> : Controller where TIdentityKey:IEquatable<TIdentityKey> where TClientKey:IEquatable<TClientKey> { private readonly UserManager<ApplicationUser<TIdentityKey,TClientKey>> _userManager; public BaseUserInfoController(UserManager<ApplicationUser<TIdentityKey,TClientKey>> userManager) => _userManager = userManager; // GET: /api/userinfo [Authorize(AuthenticationSchemes = OpenIddictServerAspNetCoreDefaults.AuthenticationScheme)] [HttpGet("~/connect/userinfo"), HttpPost("~/connect/userinfo"), Produces("application/json")] public virtual async Task<IActionResult> Userinfo() { var user = await _userManager.GetUserAsync(User); if (user == null) { return Challenge( authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, properties: new AuthenticationProperties(new Dictionary<string, string> { [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidToken, [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The specified access token is bound to an account that no longer exists." })); } var claims = new Dictionary<string, object>(StringComparer.Ordinal) { [Claims.Subject] = await _userManager.GetUserIdAsync(user) }; if (User.HasScope(Scopes.Email)) { claims[Claims.Email] = await _userManager.GetEmailAsync(user); claims[Claims.EmailVerified] = await _userManager.IsEmailConfirmedAsync(user); } if (User.HasScope(Scopes.Phone)) { claims[Claims.PhoneNumber] = await _userManager.GetPhoneNumberAsync(user); claims[Claims.PhoneNumberVerified] = await _userManager.IsPhoneNumberConfirmedAsync(user); } if (User.HasScope(Scopes.Roles)) { List<string> roles = new List<string> { "dataEventRecords", "dataEventRecords.admin", "admin", "dataEventRecords.user" }; } return Ok(claims); } }
3. 自定义IUnitOfWork需指定全部泛型键参数
自定义的工作单元接口需要绑定所有泛型键类型,使用时必须传入全部参数,非常繁琐:
public interface IUnitOfWork<TRoleKey, TUserKey, TClientKey> : IDisposable where TRoleKey : IEquatable<TRoleKey> where TUserKey : IEquatable<TUserKey> where TClientKey : IEquatable<TClientKey> { IUserService<TRoleKey, TUserKey, TClientKey> UserService { get; } IRoleService<TRoleKey, TUserKey, TClientKey> RoleService { get; } IUserRoleService<TRoleKey, TUserKey, TClientKey> UserRoleService { get; } IRolePermissionService<TRoleKey, TUserKey, TClientKey> RolePermissionService { get; } Task<bool> Commit(); }
尝试的解决方案:标记接口
为了避免到处定义泛型参数,我考虑为泛型类添加标记接口,以ApplicationUser为例:
public interface IMarkerApplicationUser{} public class ApplicationUser<TIdentityKey, TClientKey> : IMarkerApplicationUser, IdentityUser<TIdentityKey>, IApplicationUser<TIdentityKey, TClientKey> where TIdentityKey : IEquatable<TIdentityKey> where TClientKey : IEquatable<TClientKey> { public TClientKey TenantId { get; set; } }
之后在依赖注入中指定具体的泛型实现,这样在页面或控制器中就可以通过标记接口注入,无需定义泛型:
services.AddScoped<IMarkerApplicationUser, ApplicationUser<Guid,Guid>>();
但我了解到标记接口通常被认为是不良实践,因此想咨询:这种方案是否可行?
我的核心目标是构建可复用的通用微服务(如用户管理、角色管理、审计管理、异常管理),允许主项目传入键类型,同时避免在所有项目中统一使用GUID作为主键(部分系统有空间限制,无需使用GUID)。
内容的提问来源于stack exchange,提问作者Safi Mustafa
相关产品推荐
相关产品推荐

