标记接口以避免泛型控制器和带有泛型参数的构造函数。

6

我已覆盖了微软身份验证提供的默认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;
        }
    }

我已经将这些内容制作在一个类库中并导入到不同的项目中。这样我就可以根据项目需求为用户提供不同的键,例如基于 Guid、int、字符串等。我遇到的问题是,当我尝试在身份验证页面(如 ConfirmPassword 页面)中使用它们时,我需要在模型中指定泛型,以便我可以使用依赖注入来进行控制。

    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();
        }
    }

当我这样指定通用类型时,我无法在Razor页面内使用它,因为Razor页面不支持通用类型。
@page
@model ConfirmEmailModel<T>// SYNTAX ERROR
@{
    ViewData["Title"] = "Confirm email";
}

<h1>@ViewData["Title"]</h1>

另一个问题是当我尝试在控制器中使用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);
        }

    }

我写了一个 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();
    }

为了解决所有这些问题,我考虑使用标记接口来处理不同的服务,例如使用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; }
        
    }

之后,我只需将它们作为构造参数并使用依赖注入来指定通用函数和类,而不是GenericType。

services.AddScoped<IMarkerApplicationUser, ApplicationUser<Guid,Guid>>();

这是一种好的方法吗?我已经看到很多地方说使用标记接口是不好的实践。

这样做的主要目的是为我的常见项目创建通用的微服务。像用户管理、角色管理、审计管理、异常管理等,然后从主项目传递键类型。我不想在每个地方都使用GUID作为主键,因为一些系统没有使用Guid的要求,并且有空间限制。


嗯,是的,市场接口是不好的实践。但是如果您给它一些常用属性,那么最终你会得到一个正常的接口,这是可以接受的。只要确保您不会重建整个用户存储等内容即可。 - Stefan
@Stefan,接口包装器和标记接口有什么区别?它们是一样的吗? - Safi Mustafa
接口“包装器”封装了常见逻辑,并让您处理不同的实现...棘手的部分是解决正确的实现,所以我不确定它是否有助于您的情况。 - Stefan
1
@Stefan,我的情况中没有太多的共同逻辑。我只需要避免在控制器和其他模型中到处指定TKey。只需在一个地方使用依赖注入指定它们即可。我想不到其他方法,除了使用MarkerInterfaces。 - Safi Mustafa
让我们在聊天中继续这个讨论 - Safi Mustafa
显示剩余3条评论
1个回答

0

ConfirmEmailModel 不需要是泛型的

这些是我能看到受到 ConfirmEmailModel 泛型类型参数影响的唯一事物:

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

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

你不需要泛型类型参数来使这些部分配合起来。如果你的 ConfirmEmailModel 看起来像下面这样会怎么样?

public class ConfirmEmailModel : PageModel
{
    readonly IUserManager _userManager;

    public ConfirmEmailModel(IUserManager 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();
    }
}

public interface IUserManager
{
    Task<Result> ConfirmEmailAsync(object user, string code);

    Task<object?> FindByIdAsync(string userId);
}

sealed class UserManagerAdapter : IUserManager
{
    readonly UserManager<ApplicationUser<TIdentityKey,TClientKey>> _userManager;

    public UserManagerAdapter(UserManager<ApplicationUser<TIdentityKey,TClientKey>> userManager)
    {
        _userManager = userManager;
    }

    public async Task<Result> ConfirmEmailAsync(object user, string code)
    {
        if (user is not ApplicationUser<TIdentityKey,TClientKey> applicationUser)
            return Fail();
        return await _userManager.ConfirmEmailAsync(applicationUser, code);
    }

    public async Task<object?> FindByIdAsync(string userId)
    {
        return await _userManager.FindByIdAsync(userId);
    }
}

然后,您可以以这样的方式连接您的IoC注册表,使得UserManagerAdapterIUserManager的注册实现。

BaseUserInfoController也不需要是通用的

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

通用类型参数实际上用于什么?

对我来说,唯一关心user变量类型的是第一个给出它的_userManager。这在我的头脑中引发了一个小警告标志,提示“实现细节”。从您的BaseUserInfoController的角度来看,该类型并不重要,因此泛型类型参数有什么意义?如果_userManager只返回不透明的object,那么您可以丢弃泛型类型参数,并且您的BaseUserInfoController不会变差。

漏斗抽象

我认为你的抽象有点泄漏(谷歌短语“leaky abstraction”)。在你的情况下,通用类型参数是一种实现细节,甚至你的模型和控制器也不关心它们,但是使用你的模型或控制器的所有内容都必须处理这些细节。

相反,我建议你将这些实现细节隐藏在专门针对这些接口的消费者的接口后面。

希望这可以帮助你!

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


这就是为什么我在问是否应该使用标记接口。这样,我就不需要在各处传递泛型类型,而只需要传递一个对象。我不认为知道你需要传递ApplicationUser的类型有多泄露,这并不是一个坏事情,我没有泄漏仓库内部的具体细节。你只需要指定你的对象,它会在幕后执行其预期功能,或者你可以选择覆盖它。 - Safi Mustafa
@SafiMustafa 我认为你可以通过将用户_manager_放在接口后面来完成所有这些事情。记住:要根据消费者定制接口。例如,ConfirmEmailModel需要哪些接口签名?(续) - Matt Thomas
我猜你是建议我使用适配器模式。让我对此进行一些研究。 - Safi Mustafa
@SafiMustafa(续)所以我想在这种特定情况下,适配器模式是你想要的。但是一般规则是“为消费者量身定制接口”。当你遵循这个一般规则时,许多其他问题都会消失,它适用范围不仅限于C# interface。例如,当你将该规则应用于为多个企业提供服务的HTTP API端点时,你会得到每个企业一个HTTP端点,这样可以避免每个企业的规则直接接触代码中的另一个企业...节省了很多麻烦。我发现这个一般规则自然有助于编写SOLID代码。 - Matt Thomas
适配器也无法工作。您在IUserManager中使用了对象,那么IUnitOfWork怎么办?我们该如何处理? - Safi Mustafa
显示剩余5条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接