提问人:Safi Mustafa 提问时间:8/30/2022 最后编辑:Safi Mustafa 更新时间:9/19/2022 访问量:371
标记接口:避免使用泛型参数的泛型控制器和构造函数
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;
}
}
我已经在类库中制作了这些内容,并将其导入到不同的项目中。这样我就可以根据项目需要为用户提供不同的 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 并且有空间限制。
答:
ConfirmEmailModel
不需要是通用的
这些是我能看到的唯一受泛型类型影响的东西
中的参数:ConfirmEmailModel
- 返回类型
_userManager.FindByIdAsync(...)
- 变量的类型
user
OnGetAsync(...)
- 中参数的类型
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 注册。UserManagerAdapter
IUserManager
BaseUserInfoController
也不需要是通用的
您可以将类似的思维方式应用于 。BaseUserInfoController
泛型类型参数的实际用途是什么?
在我看来,似乎唯一真正关心变量是什么类型的,就是在第一个变量中给你的
地方。这在我的脑海中引发了一个小警告标志,上面写着“实施
细节”。从你的角度来看,这并不重要
该类型是什么,那么泛型类型参数的意义何在?如果只是返回不透明的 s,那么你可能会丢失泛型
键入参数,您的情况不会更糟。user
_userManager
BaseUserInfoController
_userManager
object
BaseUserInfoController
泄漏的抽象
我认为你的抽象有点漏水(谷歌“漏水”这句话 抽象“)。在本例中,泛型类型参数是一种实现 细节 - 甚至你的模型和控制器都不关心它们是什么 - 然而 使用模型或控制器的所有内容都必须处理这些细节。
相反,我的建议是将这些实现细节隐藏在后面 为这些接口的使用者定制的接口。
我希望这会有所帮助!
上面的代码是随手写的,可能无法编译
评论
ConfirmEmailModel
object
ConfirmEmailModel
interface IReportModel { }
public Report Generate(IReportModel model)
public Report Generate(object model)
评论