簡體   English   中英

標記接口避免使用泛型參數的泛型控制器和構造器

[英]Marker Interface To avoid Generic Controllers & Constructor with generic paramaters

我已經覆蓋了 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;
        }
    }

我在 class 庫中制作了這些內容,並導入到不同的項目中。 這樣我就可以根據項目需要為用戶提供不同的Keys,如Guid、int、string。 我面臨的問題是,當我嘗試在諸如 ConfirmPassword 頁面之類的身份頁面中使用這些時,我需要在 model 中指定通用,以便我可以使用依賴注入來控制它。

    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>

其他問題是當我嘗試在 controller 中使用 SignInManager 或 UserStore 時。 我再次不能使用依賴注入在某些地方注入 generics

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

    }

要解決這兩個問題。 我正在考慮為 ApplicationUser 使用 MarkerInterfaces。

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

這是一個好方法嗎? 我已經閱讀了所有使用標記接口是一種不好的做法的地方。

ConfirmEmailModel不需要是通用的

這些是我能看到的唯一受ConfirmEmailModel中的泛型類型參數影響的東西:

  • _userManager.FindByIdAsync(...)的返回類型
  • OnGetAsync(...)user變量的類型
  • _userManager.ConfirmEmailAsync(user, ...)user參數的類型

(此外,對於您的 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

泛型類型參數實際用於什么?

對我來說,唯一真正關心user變量是什么類型的似乎是最初給你的_userManager 這在我的腦海中引發了一個小小的警告標志,上面寫着“實施細節”。 從您的BaseUserInfoController的角度來看,該類型是什么並不重要,那么泛型類型參數的意義何在? 如果_userManager剛剛返回不透明的object ,那么您可能會丟失泛型類型參數,並且您的BaseUserInfoController不會更糟。

泄漏的抽象

我認為您的抽象有點泄漏(谷歌短語“泄漏抽象”)。 Generic type parameters in your case are an implementation detail—not even your model and controller care what they are—and yet everything that uses your model or controller has to deal with those details.

相反,我建議您將那些實現細節隱藏在為這些接口的消費者量身定制的接口后面。

我希望這有幫助!

上面的代碼是臨時寫的,可能無法編譯

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM