标记接口:避免使用泛型参数的泛型控制器和构造函数

Marker Interface To avoid Generic Controllers & Constructor with generic paramaters

提问人:Safi Mustafa 提问时间:8/30/2022 最后编辑:Safi Mustafa 更新时间:9/19/2022 访问量:371

问:

我覆盖了 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;
        }
    }

我已经在类库中制作了这些内容,并将其导入到不同的项目中。这样我就可以根据项目需要为用户提供不同的 Key,例如 Guid、int、string。我面临的问题是,当我尝试在 Identity Pages(例如 ConfirmPassword Page)中使用它们时,我需要在模型中指定 Generic,以便我可以使用依赖注入来控制它。

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

当我像这样指定泛型类型时。我不能在剃刀页面中使用它,因为剃刀页面不支持泛型类型。

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

解决所有这些问题。我正在考虑将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; }
        
    }

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

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

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

做这一切的主要目的是为我的常见项目创建通用的微服务。像 UserManagement、RoleManagement、Audit Management、Exception Management 一样,然后从主项目中传递密钥类型。我不想在任何地方都使用 GUID 作为主键,因为某些系统不需要使用 Guid 并且有空间限制。

C# ASP.net-core 依赖项注入 asp.net 标识 标记接口

评论

0赞 Stefan 8/30/2022
嗯,是的,市场接口是不好的做法。但是,如果你给它一些常用的属性,你最终会得到一个普通的界面,这很好,只要确保你没有重建完整的用户存储等等。
0赞 Safi Mustafa 8/30/2022
@Stefan 如果我把它留空或只是在没有任何确定理由的情况下添加一些属性,它会有什么不同?这些属性的用途是什么?
0赞 Stefan 8/30/2022
如果使用接口包装器,则可能还可以修复 razor 泛型类型问题。不确定
0赞 Safi Mustafa 8/30/2022
@Stefan 接口包装器和标记器接口有什么区别?两者是一样的吗?
1赞 Safi Mustafa 8/30/2022
@Stefan就我而言,没有太多的共同逻辑。我只需要避免像在控制器和其他模型中那样在任何地方指定 TKey。只需使用依赖注入在一个地方指定它们即可。除了使用MarkerInterfaces之外,我想不出任何其他方法

答:

0赞 Matt Thomas 9/6/2022 #1

ConfirmEmailModel不需要是通用的

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

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

(此外,必须是 null 检查的可为 null 的引用类型)user

不需要泛型类型参数即可使这些部分组合在一起。什么 如果你的反面看起来像下面?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

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

在我看来,似乎唯一真正关心变量是什么类型的,就是在第一个变量中给你的 地方。这在我的脑海中引发了一个小警告标志,上面写着“实施 细节”。从你的角度来看,这并不重要 该类型是什么,那么泛型类型参数的意义何在?如果只是返回不透明的 s,那么你可能会丢失泛型 键入参数,您的情况不会更糟。user_userManagerBaseUserInfoController_userManagerobjectBaseUserInfoController

泄漏的抽象

我认为你的抽象有点漏水(谷歌“漏水”这句话 抽象“)。在本例中,泛型类型参数是一种实现 细节 - 甚至你的模型和控制器都不关心它们是什么 - 然而 使用模型或控制器的所有内容都必须处理这些细节。

相反,我的建议是将这些实现细节隐藏在后面 为这些接口的使用者定制的接口。

我希望这会有所帮助!

上面的代码是随手写的,可能无法编译

评论

0赞 Safi Mustafa 9/8/2022
这就是为什么我问我是否应该使用标记界面。这样,我就不需要到处传递泛型类型而不是对象。我不认为知道您需要传递ApplicationUser类型并不是一件坏事,我不会泄露存储库中发生的事情。你只需要指定你的对象,它就会在引擎盖下做它打算做的事情,或者你可以选择覆盖它。
0赞 Matt Thomas 9/8/2022
@SafiMustafa我认为你可以通过将用户管理器放在界面后面来做所有这些事情。请记住:为消费者量身定制界面。例如,需要哪些接口签名?(续)ConfirmEmailModel
0赞 Matt Thomas 9/8/2022
@SafiMustafa 关于用户,我认为答案是“ConfirmEmailModel 根本不需要接口”(因为 ConfirmEmailModel 从不调用用户对象......它需要知道的是它是一个可为 null 的 )。但是对于用户管理器,需要调用几个方法。因此,将用户管理器放在界面后面,那么事情就很简单了——所有的泛型都消失了:)objectConfirmEmailModel
0赞 Matt Thomas 9/8/2022
@SafiMustafa 特别是关于标记接口:当我需要在类型系统中引入层次结构但不需要任何通用行为时,我发现它们很有用。例如,我可以在基于反射的报告生成系统中的所有模型上创建并实现该接口。然后我的报告生成器可以有一个带有此签名的函数:而不是此签名:。标记界面向每个人暗示,这些类型(不是任何对象)适用于我的框架interface IReportModel { }public Report Generate(IReportModel model)public Report Generate(object model)
0赞 Safi Mustafa 9/8/2022
我猜你是在向我建议适配器模式。让我对此做一些研究。