You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

采用标记接口解决泛型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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.27 19:27:37