标记接口以避免使用通用参数的通用控制器和构造函数

Saf*_*afa 6 c# dependency-injection marker-interfaces asp.net-identity asp.net-core

我已覆盖 Microsoft Identity 提供的默认 IdentityUser 和 UserStore。

    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;
        }
    }
Run Code Online (Sandbox Code Playgroud)

我已将它们放入类库中并导入到不同的项目中。这样我就可以根据项目需要为用户提供不同的Key,例如Guid、int、string。我面临的问题是,当我尝试在身份页面(例如确认密码页面)中使用这些内容时,我需要在模型中指定通用,以便我可以使用依赖项注入来控制它。

    public class ConfirmEmailModel<TIdentityKey,TClientKey> : PageModel
    where TIdentityKey:IEqutable<TIdentityKey>
    where TClientKey:IEqutable<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();
        }
    }
Run Code Online (Sandbox Code Playgroud)

当我像这样指定通用类型时。我无法在 razor 页面内使用它,因为 razor 页面不支持泛型类型。

@page
@model ConfirmEmailModel<T>// SYNTAX ERROR
@{
    ViewData["Title"] = "Confirm email";
}

<h1>@ViewData["Title"]</h1>
Run Code Online (Sandbox Code Playgroud)

其他问题是当我尝试在控制器内使用 SignInManager 或 UserStore 时。我再次无法使用依赖注入在某些地方注入泛型

Public class BaseUserInfoController<TIdentityKey,TClientKey> : Controller
where TIdentityKey:IEqutable<TIdentityKey>
where TClientKey:IEqutable<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)
            {
                // Note: the "sub" claim is a mandatory claim and must be included in the JSON response.
                [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))
            {
                //claims[Claims.Role] = await _userManager.GetRolesAsync(user);
                List<string> roles = new List<string> { "dataEventRecords", "dataEventRecords.admin", "admin", "dataEventRecords.user" };
            }

            // Note: the complete list of standard claims supported by the OpenID Connect specification
            // can be found here: http://openid.net/specs/openid-connect-core-1_0.html#StandardClaims

            return Ok(claims);
        }

    }
Run Code Online (Sandbox Code Playgroud)

对于另一项服务,我编写了一个 IUnitOfWork。在控制器内使用此 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();
    }
Run Code Online (Sandbox Code Playgroud)

为了解决所有这些问题。我正在考虑将 MarkerInterfaces 用于所有这些不同的服务。例如使用 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; }
        
    }
Run Code Online (Sandbox Code Playgroud)

之后我可以将它们作为构造函数参数并使用依赖项注入来指定泛型而不是 GenericType 函数和类。

services.AddScoped<IMarkerApplicationUser, ApplicationUser<Guid,Guid>>();
Run Code Online (Sandbox Code Playgroud)

这是一个好方法吗?我读过所有关于使用标记接口是一种不好的做法的地方。

做这一切的主要目的是为我的常见项目创建通用的微服务。像用户管理、角色管理、审计管理、异常管理,然后从主项目传递密钥类型。我不想到处使用 GUID 作为主键,因为某些系统不需要使用 Guid 并且有空间限制。

Mat*_*mas 0

ConfirmEmailModel不需要是通用的

\n

这些是我能看到的唯一受通用类型\n参数影响的东西ConfirmEmailModel

\n
    \n
  • 返回类型为_userManager.FindByIdAsync(...)
  • \n
  • user中变量的类型OnGetAsync(...)
  • \n
  • user中参数的类型_userManager.ConfirmEmailAsync(user, ...)
  • \n
\n

(此外,user必须是可空引用类型才能进行空检查)

\n

您不需要泛型类型参数来使这些部分组合在一起。\n如果您的ConfirmEmailModel内容如下所示怎么办?

\n
public class ConfirmEmailModel : PageModel\n{\n    readonly IUserManager _userManager;\n\n    public ConfirmEmailModel(IUserManager userManager)\n    {\n        _userManager = userManager;\n    }\n\n    [TempData]\n    public virtual string StatusMessage { get; set; }\n\n    public virtual async Task<IActionResult> OnGetAsync(string userId, string code)\n    {\n        if (userId == null || code == null)\n        {\n            return RedirectToPage("/Index");\n        }\n\n        var user = await _userManager.FindByIdAsync(userId);\n        if (user == null)\n        {\n            return NotFound($"Unable to load user with ID \'{userId}\'.");\n        }\n\n        code = Encoding.UTF8.GetString(WebEncoders.Base64UrlDecode(code));\n        var result = await _userManager.ConfirmEmailAsync(user, code);\n        StatusMessage = result.Succeeded\n            ? "Thank you for confirming your email."\n            : "Error confirming your email.";\n        return Page();\n    }\n}\n\npublic interface IUserManager\n{\n    Task<Result> ConfirmEmailAsync(object user, string code);\n\n    Task<object?> FindByIdAsync(string userId);\n}\n\nsealed class UserManagerAdapter : IUserManager\n{\n    readonly UserManager<ApplicationUser<TIdentityKey,TClientKey>> _userManager;\n\n    public UserManagerAdapter(UserManager<ApplicationUser<TIdentityKey,TClientKey>> userManager)\n    {\n        _userManager = userManager;\n    }\n\n    public async Task<Result> ConfirmEmailAsync(object user, string code)\n    {\n        if (user is not ApplicationUser<TIdentityKey,TClientKey> applicationUser)\n            return Fail();\n        return await _userManager.ConfirmEmailAsync(applicationUser, code);\n    }\n\n    public async Task<object?> FindByIdAsync(string userId)\n    {\n        return await _userManager.FindByIdAsync(userId);\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

然后你可以以这样的方式连接你的 IoC 注册,\nUserManagerAdapterIUserManager.

\n

BaseUserInfoController也不需要是通用的

\n

您可以将类似的思维方式应用于BaseUserInfoController.

\n

泛型类型参数实际用途是什么?

\n

对我来说,似乎唯一真正关心变量user是什么类型的事情,就是_userManager最初给你的变量的类型。这在我的脑海中升起了一个小警告标志,上面写着“实施\n细节”。从您的角度来看,BaseUserInfoController该类型是什么并不重要,那么泛型类型参数的意义何在?如果只是_userManager返回不透明的object,那么您可能会丢失通用类型参数,并且您的情况BaseUserInfoController不会更糟。

\n

抽象泄漏

\n

我认为你的抽象有点泄漏(谷歌搜索短语“leaky\nabstraction”)。在您的情况下,通用类型参数是一个实现\n细节\xe2\x80\x94,甚至您的模型和控制器都不关心它们是什么\xe2\x80\x94,但是\所有使用您的模型或控制器的东西都必须处理这些细节。

\n

相反,我建议您将这些实现细节隐藏在针对这些接口的使用者量身定制的接口后面。

\n

我希望这有帮助!

\n

上面的代码是即兴编写的,可能无法编译

\n