From fd039607fa9fbf1cb68174cdb9edf970c513b9a9 Mon Sep 17 00:00:00 2001 From: MSingh-13 <106232578+MSingh-13@users.noreply.github.com> Date: Thu, 27 Jun 2024 18:48:35 +0300 Subject: [PATCH 1/9] Updated editor and display taghelpers to accept class and style attributes. Updated global.json "rollForward": "latestMajor" because did not have sdk verison 7 installed. --- global.json | 2 +- src/TagHelperPack/DisplayTagHelper.cs | 16 ++++++- src/TagHelperPack/EditorTagHelper.cs | 20 ++++++-- src/TagHelperPack/HtmlHelperExtensions.cs | 57 +++++++++++++++++++++++ 4 files changed, 90 insertions(+), 5 deletions(-) diff --git a/global.json b/global.json index 8cce0fe..30e2afc 100644 --- a/global.json +++ b/global.json @@ -1,7 +1,7 @@ { "sdk": { "version": "7.0.100", - "rollForward": "latestFeature", + "rollForward": "latestMajor", "allowPrerelease": false } } \ No newline at end of file diff --git a/src/TagHelperPack/DisplayTagHelper.cs b/src/TagHelperPack/DisplayTagHelper.cs index 9265fec..89656ee 100644 --- a/src/TagHelperPack/DisplayTagHelper.cs +++ b/src/TagHelperPack/DisplayTagHelper.cs @@ -62,6 +62,18 @@ public IDictionary ViewData [ViewContext] public ViewContext ViewContext { get; set; } + /// + /// css class + /// + [HtmlAttributeName("class")] + public string Class { get; set; } + + /// + /// css style + /// + [HtmlAttributeName("style")] + public string Style { get; set; } + /// public override void Process(TagHelperContext context, TagHelperOutput output) { @@ -82,7 +94,9 @@ public override void Process(TagHelperContext context, TagHelperOutput output) ((IViewContextAware)_htmlHelper).Contextualize(ViewContext); - output.Content.SetHtmlContent(_htmlHelper.Display(For, HtmlFieldName, TemplateName, ViewData)); + var cssViewData = new { htmlAttributes = new { @class = Class, style = Style } }; + var finalViewData = _htmlHelper.MergeHtmlAttributes(cssViewData, ViewData); + output.Content.SetHtmlContent(_htmlHelper.Display(For, HtmlFieldName, TemplateName, finalViewData)); output.TagName = null; } diff --git a/src/TagHelperPack/EditorTagHelper.cs b/src/TagHelperPack/EditorTagHelper.cs index 5784f41..5b563fa 100644 --- a/src/TagHelperPack/EditorTagHelper.cs +++ b/src/TagHelperPack/EditorTagHelper.cs @@ -55,13 +55,25 @@ public IDictionary ViewData set => _viewData = value; } - /// + /// /// Gets or sets the . /// [HtmlAttributeNotBound] [ViewContext] public ViewContext ViewContext { get; set; } + /// + /// css class + /// + [HtmlAttributeName("class")] + public string Class { get; set; } + + /// + /// css style + /// + [HtmlAttributeName("style")] + public string Style { get; set; } + /// public override void Process(TagHelperContext context, TagHelperOutput output) { @@ -82,8 +94,10 @@ public override void Process(TagHelperContext context, TagHelperOutput output) ((IViewContextAware)_htmlHelper).Contextualize(ViewContext); - output.Content.SetHtmlContent(_htmlHelper.Editor(For, HtmlFieldName, TemplateName, ViewData)); + var cssViewData = new { htmlAttributes = new { @class = Class, style = Style } }; + var finalViewData = _htmlHelper.MergeHtmlAttributes(cssViewData, ViewData); + output.Content.SetHtmlContent(_htmlHelper.Editor(For, HtmlFieldName, TemplateName, finalViewData)); output.TagName = null; } -} +} \ No newline at end of file diff --git a/src/TagHelperPack/HtmlHelperExtensions.cs b/src/TagHelperPack/HtmlHelperExtensions.cs index 65d440d..525d2e9 100644 --- a/src/TagHelperPack/HtmlHelperExtensions.cs +++ b/src/TagHelperPack/HtmlHelperExtensions.cs @@ -3,6 +3,9 @@ using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Html; using TagHelperPack; +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNetCore.Routing; namespace Microsoft.AspNetCore.Mvc.ViewFeatures; @@ -95,4 +98,58 @@ public static IHtmlContent Editor(this IHtmlHelper htmlHelper, ModelExpression m return htmlHelper.Editor(expression, templateName, htmlFieldName, additionalViewData); } + + /// + /// Merge values from 2 anonymous or IDictionary objects. Values of overlapping keys from the 'existing values' object are replaced by the values + /// from the 'new values' object except if the keys are 'class' or 'style', in which case the values are concatentated with a space or ; respectively. + /// + /// new values + /// existing values + /// + internal static IDictionary MergeHtmlAttributes(this IHtmlHelper helper, object newHtmlAttributesObject, object existingHtmlAttributesObject) + { + var keysConcatValuesWithSpace = new string[] { "class" }; + var keysConcatValuesWithSemiColon = new string[] { "style" }; + + var htmlAttributesDict = newHtmlAttributesObject as IDictionary; + var defaultHtmlAttributesDict = existingHtmlAttributesObject as IDictionary; + + IDictionary htmlAttributes = (htmlAttributesDict != null) + ? new RouteValueDictionary(htmlAttributesDict) + : HtmlHelper.AnonymousObjectToHtmlAttributes(newHtmlAttributesObject); + + IDictionary existingHtmlAttributes = (defaultHtmlAttributesDict != null) + ? new RouteValueDictionary(defaultHtmlAttributesDict) + : HtmlHelper.AnonymousObjectToHtmlAttributes(existingHtmlAttributesObject); + + foreach (var item in htmlAttributes) + { + if (keysConcatValuesWithSpace.Contains(item.Key)) + { + existingHtmlAttributes.TryGetValue(item.Key, out object? value); + if (value != null && item.Value != null) + { + existingHtmlAttributes[item.Key] = value != null ? + string.Format("{0} {1}", existingHtmlAttributes[item.Key], item.Value) + : item.Value; + } + } + else if (keysConcatValuesWithSemiColon.Contains(item.Key)) + { + existingHtmlAttributes.TryGetValue(item.Key, out object? value); + if (value != null && item.Value != null) + { + existingHtmlAttributes[item.Key] = value != null ? + string.Format("{0}; {1}", existingHtmlAttributes[item.Key], item.Value) + : item.Value; + } + } + else + { + existingHtmlAttributes[item.Key] = item.Value; + } + } + + return existingHtmlAttributes; + } } From 4d2de242bf8df69953e9e006d0c70bba68a8819f Mon Sep 17 00:00:00 2001 From: MSingh-13 <106232578+MSingh-13@users.noreply.github.com> Date: Thu, 27 Jun 2024 18:48:36 +0300 Subject: [PATCH 2/9] Updated AuthzTagHelper to handle authorizations based on permissions (using string resource with the specified policy) and roles (Principal.IsInRole()). --- .../TagHelperPack.Sample/Pages/Index.cshtml | 4 ++ .../TagHelperPack.Sample/QueryAuthScheme.cs | 2 + samples/TagHelperPack.Sample/Startup.cs | 19 ++++++ src/TagHelperPack/AuthzTagHelper.cs | 58 +++++++++++++++---- 4 files changed, 73 insertions(+), 10 deletions(-) diff --git a/samples/TagHelperPack.Sample/Pages/Index.cshtml b/samples/TagHelperPack.Sample/Pages/Index.cshtml index fee5589..9df3719 100644 --- a/samples/TagHelperPack.Sample/Pages/Index.cshtml +++ b/samples/TagHelperPack.Sample/Pages/Index.cshtml @@ -327,6 +327,10 @@ public Customer Customer { get; set; }
This will only render if the user is authenticated.
This will only render when the user is *not* authenticated.
This will only render if the user is authenticated and authorized via the "AdminPolicy" policy.
+
This will only render if the user has "ViewUsers" permission.
+
This will only render if the user has "ManageUsers" permission.
+
This will only render if the user belongs to the "standard" role.
+
This will only render if the user belongs to the "admin" role.

Source

diff --git a/samples/TagHelperPack.Sample/QueryAuthScheme.cs b/samples/TagHelperPack.Sample/QueryAuthScheme.cs index cd77266..1d5f876 100644 --- a/samples/TagHelperPack.Sample/QueryAuthScheme.cs +++ b/samples/TagHelperPack.Sample/QueryAuthScheme.cs @@ -28,10 +28,12 @@ protected override Task HandleAuthenticateAsync() { identity.AddClaim(new Claim("Name", "AdminUser")); identity.AddClaim(new Claim("IsAdmin", "true")); + identity.AddClaim(new Claim(ClaimTypes.Role, "admin")); } else { identity.AddClaim(new Claim("Name", "StandardUser")); + identity.AddClaim(new Claim(ClaimTypes.Role, "standard")); } var user = new ClaimsPrincipal(identity); return Task.FromResult(AuthenticateResult.Success(new AuthenticationTicket(user, nameof(QueryAuthScheme)))); diff --git a/samples/TagHelperPack.Sample/Startup.cs b/samples/TagHelperPack.Sample/Startup.cs index 407937a..72077bd 100644 --- a/samples/TagHelperPack.Sample/Startup.cs +++ b/samples/TagHelperPack.Sample/Startup.cs @@ -53,6 +53,25 @@ public void ConfigureServices(IServiceCollection services) return httpContext is not null; }); }); + options.AddPolicy("PermissionPolicy", policy => + { + List standardPermissions = new List() { "ViewUsers"}; + List adminPermissions = new List(standardPermissions) { "ManageUsers" }; + + policy.RequireAuthenticatedUser(); + policy.RequireAssertion(handler => + { + var permission = handler.Resource as string; + if (handler.User.IsInRole("admin")) + { + return adminPermissions.Contains(permission); + } + else //standard role + { + return standardPermissions.Contains(permission); + } + }); + }); }); // Optional optimizations to avoid Reflection diff --git a/src/TagHelperPack/AuthzTagHelper.cs b/src/TagHelperPack/AuthzTagHelper.cs index fd83588..878ad7a 100644 --- a/src/TagHelperPack/AuthzTagHelper.cs +++ b/src/TagHelperPack/AuthzTagHelper.cs @@ -12,6 +12,8 @@ namespace TagHelperPack; ///
[HtmlTargetElement("*", Attributes = AspAuthzAttributeName)] [HtmlTargetElement("*", Attributes = AspAuthzPolicyAttributeName)] +[HtmlTargetElement("*", Attributes = AspAuthzRoleAttributeName)] +[HtmlTargetElement("*", Attributes = AspAuthzPolicyPermissionAttributeName)] public class AuthzTagHelper : TagHelper { internal static object SuppressedKey = new(); @@ -19,6 +21,8 @@ public class AuthzTagHelper : TagHelper private const string AspAuthzAttributeName = "asp-authz"; private const string AspAuthzPolicyAttributeName = "asp-authz-policy"; + private const string AspAuthzRoleAttributeName = "asp-authz-role"; + private const string AspAuthzPolicyPermissionAttributeName = "asp-authz-permission"; private readonly IAuthorizationService _authz; @@ -49,6 +53,18 @@ public AuthzTagHelper(IAuthorizationService authz) [HtmlAttributeName(AspAuthzPolicyAttributeName)] public string RequiredPolicy { get; set; } + /// + /// A role that the User must belong to in order for the current element to be rendered. + /// + [HtmlAttributeName(AspAuthzRoleAttributeName)] + public string RequiredRole { get; set; } + + /// + /// A permission that must be satisfied in order for the current element to be rendered. asp-authz-policy should be set as well. + /// + [HtmlAttributeName(AspAuthzPolicyPermissionAttributeName)] + public string RequiredPolicyPermission { get; set; } + /// /// Gets or sets the . /// @@ -74,7 +90,7 @@ public override async Task ProcessAsync(TagHelperContext context, TagHelperOutpu return; } - var requiresAuth = RequiresAuthentication || !string.IsNullOrEmpty(RequiredPolicy); + var requiresAuth = RequiresAuthentication || !string.IsNullOrEmpty(RequiredPolicy) || !string.IsNullOrEmpty(RequiredRole); var showOutput = false; var user = ViewContext.HttpContext.User; @@ -85,23 +101,45 @@ public override async Task ProcessAsync(TagHelperContext context, TagHelperOutpu } else if (!string.IsNullOrEmpty(RequiredPolicy)) { - // auth-policy="foo" & user is authorized for policy "foo" - var cacheKey = AspAuthzPolicyAttributeName + "." + RequiredPolicy; bool authorized; - var cachedResult = ViewContext.ViewData[cacheKey]; - if (cachedResult != null) + if (!string.IsNullOrEmpty(RequiredPolicyPermission)) { - authorized = (bool)cachedResult; + // auth-policy="foo" & user is authorized for policy "foo" based on permission + var cacheKey = AspAuthzPolicyPermissionAttributeName + "." + RequiredPolicyPermission; + var cachedResult = ViewContext.ViewData[cacheKey]; + if (cachedResult != null) + { + authorized = (bool)cachedResult; + } + else + { + var authResult = await _authz.AuthorizeAsync(user, RequiredPolicyPermission, RequiredPolicy); + authorized = authResult.Succeeded; + ViewContext.ViewData[cacheKey] = authorized; + } } else { - var authResult = await _authz.AuthorizeAsync(user, ViewContext, RequiredPolicy); - authorized = authResult.Succeeded; - ViewContext.ViewData[cacheKey] = authorized; + // auth-policy="foo" & user is authorized for policy "foo" + var cacheKey = AspAuthzPolicyAttributeName + "." + RequiredPolicy; + var cachedResult = ViewContext.ViewData[cacheKey]; + if (cachedResult != null) + { + authorized = (bool)cachedResult; + } + else + { + var authResult = await _authz.AuthorizeAsync(user, ViewContext, RequiredPolicy); + authorized = authResult.Succeeded; + ViewContext.ViewData[cacheKey] = authorized; + } } - showOutput = authorized; } + else if (!string.IsNullOrEmpty(RequiredRole)) + { + showOutput = user.IsInRole(RequiredRole); + } else if (requiresAuth && user.Identity.IsAuthenticated) { // auth="true" & user is authenticated From ac55e3c1b4565bc9dc18a258acef354db4d11106 Mon Sep 17 00:00:00 2001 From: MSingh-13 <106232578+MSingh-13@users.noreply.github.com> Date: Thu, 27 Jun 2024 18:48:36 +0300 Subject: [PATCH 3/9] Incorporated PR feedback 1. Using var to declare permissions list in Startup.cs 2. Updated the documentation text. 3. Sorted the using statements in HtmlHelperExtensions. Co-authored-by: Damian Edwards --- samples/TagHelperPack.Sample/Startup.cs | 4 ++-- src/TagHelperPack/AuthzTagHelper.cs | 2 +- src/TagHelperPack/DisplayTagHelper.cs | 4 ++-- src/TagHelperPack/EditorTagHelper.cs | 4 ++-- src/TagHelperPack/HtmlHelperExtensions.cs | 10 +++++----- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/samples/TagHelperPack.Sample/Startup.cs b/samples/TagHelperPack.Sample/Startup.cs index 72077bd..0fac120 100644 --- a/samples/TagHelperPack.Sample/Startup.cs +++ b/samples/TagHelperPack.Sample/Startup.cs @@ -55,8 +55,8 @@ public void ConfigureServices(IServiceCollection services) }); options.AddPolicy("PermissionPolicy", policy => { - List standardPermissions = new List() { "ViewUsers"}; - List adminPermissions = new List(standardPermissions) { "ManageUsers" }; + var standardPermissions = new List { "ViewUsers" }; + var adminPermissions = new List(standardPermissions) { "ManageUsers" }; policy.RequireAuthenticatedUser(); policy.RequireAssertion(handler => diff --git a/src/TagHelperPack/AuthzTagHelper.cs b/src/TagHelperPack/AuthzTagHelper.cs index 878ad7a..936801d 100644 --- a/src/TagHelperPack/AuthzTagHelper.cs +++ b/src/TagHelperPack/AuthzTagHelper.cs @@ -60,7 +60,7 @@ public AuthzTagHelper(IAuthorizationService authz) public string RequiredRole { get; set; } /// - /// A permission that must be satisfied in order for the current element to be rendered. asp-authz-policy should be set as well. + /// A permission that must be satisfied in order for the current element to be rendered. asp-authz-policy should be set as well. /// [HtmlAttributeName(AspAuthzPolicyPermissionAttributeName)] public string RequiredPolicyPermission { get; set; } diff --git a/src/TagHelperPack/DisplayTagHelper.cs b/src/TagHelperPack/DisplayTagHelper.cs index 89656ee..c946139 100644 --- a/src/TagHelperPack/DisplayTagHelper.cs +++ b/src/TagHelperPack/DisplayTagHelper.cs @@ -63,13 +63,13 @@ public IDictionary ViewData public ViewContext ViewContext { get; set; } /// - /// css class + /// CSS class name. /// [HtmlAttributeName("class")] public string Class { get; set; } /// - /// css style + /// CSS inline style. /// [HtmlAttributeName("style")] public string Style { get; set; } diff --git a/src/TagHelperPack/EditorTagHelper.cs b/src/TagHelperPack/EditorTagHelper.cs index 5b563fa..90cad0f 100644 --- a/src/TagHelperPack/EditorTagHelper.cs +++ b/src/TagHelperPack/EditorTagHelper.cs @@ -63,13 +63,13 @@ public IDictionary ViewData public ViewContext ViewContext { get; set; } /// - /// css class + /// CSS class name. /// [HtmlAttributeName("class")] public string Class { get; set; } /// - /// css style + /// CSS inline style. /// [HtmlAttributeName("style")] public string Style { get; set; } diff --git a/src/TagHelperPack/HtmlHelperExtensions.cs b/src/TagHelperPack/HtmlHelperExtensions.cs index 525d2e9..6c09f40 100644 --- a/src/TagHelperPack/HtmlHelperExtensions.cs +++ b/src/TagHelperPack/HtmlHelperExtensions.cs @@ -1,11 +1,11 @@ -using System; -using System.Reflection; +using Microsoft.AspNetCore.Html; using Microsoft.AspNetCore.Mvc.Rendering; -using Microsoft.AspNetCore.Html; -using TagHelperPack; +using Microsoft.AspNetCore.Routing; +using System; using System.Collections.Generic; using System.Linq; -using Microsoft.AspNetCore.Routing; +using System.Reflection; +using TagHelperPack; namespace Microsoft.AspNetCore.Mvc.ViewFeatures; From 4a265d2f6559f2abef720c641611527a79250307 Mon Sep 17 00:00:00 2001 From: MSingh-13 <106232578+MSingh-13@users.noreply.github.com> Date: Thu, 25 Jul 2024 19:47:40 +0300 Subject: [PATCH 4/9] Updated samples to display source for asp-authz-permission and asp-authz-role. --- samples/TagHelperPack.Sample/Pages/Index.cshtml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/samples/TagHelperPack.Sample/Pages/Index.cshtml b/samples/TagHelperPack.Sample/Pages/Index.cshtml index 9df3719..e6f6565 100644 --- a/samples/TagHelperPack.Sample/Pages/Index.cshtml +++ b/samples/TagHelperPack.Sample/Pages/Index.cshtml @@ -337,7 +337,11 @@ public Customer Customer { get; set; }
<div asp-authz>This will only render if the user is authenticated.</div>
 <div asp-authz="false">This will only render when the user is <strong>*not*</strong> authenticated.</div>
-<div asp-authz-policy="AdminPolicy">This will only render if the user is authenticated and authorized via the "AdminPolicy" policy.</div>
+<div asp-authz-policy="AdminPolicy">This will only render if the user is authenticated and authorized via the "AdminPolicy" policy.</div> +<div asp-authz-policy="PermissionPolicy" asp-authz-permission="ViewUsers">This will only render if the user has "ViewUsers" permission.</div> +<div asp-authz-policy="PermissionPolicy" asp-authz-permission="ManageUsers">This will only render if the user has "ManageUsers" permission.</div> +<div asp-authz-role="standard">This will only render if the user belongs to the "standard" role.</div> +<div asp-authz-role="admin">This will only render if the user belongs to the "admin" role.</div>

If Tag Helper

From fbdab700828f8b71e0d06cc97efe55193f772411 Mon Sep 17 00:00:00 2001 From: MSingh-13 <106232578+MSingh-13@users.noreply.github.com> Date: Fri, 28 Mar 2025 22:16:58 +0300 Subject: [PATCH 5/9] Added a condition to guard against RequiredRoles and RequiredPolicy being set at the same time in AuthzTagHelper. --- src/TagHelperPack/AuthzTagHelper.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/TagHelperPack/AuthzTagHelper.cs b/src/TagHelperPack/AuthzTagHelper.cs index 936801d..7e55447 100644 --- a/src/TagHelperPack/AuthzTagHelper.cs +++ b/src/TagHelperPack/AuthzTagHelper.cs @@ -85,6 +85,11 @@ public override async Task ProcessAsync(TagHelperContext context, TagHelperOutpu throw new ArgumentNullException(nameof(output)); } + if(!string.IsNullOrEmpty(RequiredRole) && !string.IsNullOrEmpty(RequiredPolicy)) + { + throw new InvalidOperationException($"{AspAuthzRoleAttributeName} and {AspAuthzPolicyAttributeName} cannot be set at the same time."); + } + if (context.SuppressedByAspIf() || context.SuppressedByAspAuthz()) { return; From 1ae3e047d575d413c059996700f7b7be13ab3690 Mon Sep 17 00:00:00 2001 From: MSingh-13 <106232578+MSingh-13@users.noreply.github.com> Date: Sat, 29 Mar 2025 00:08:24 +0300 Subject: [PATCH 6/9] Renamed the properties and attributes related to permission, to resource in AuthzTagHelper. Changed the type of RequiredPolicyResource property from string to object. Added a condition to ensure RequiredPolicy is set when RequiredPolicyResource is set. --- .../TagHelperPack.Sample/Pages/Index.cshtml | 8 +++---- src/TagHelperPack/AuthzTagHelper.cs | 23 +++++++++++-------- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/samples/TagHelperPack.Sample/Pages/Index.cshtml b/samples/TagHelperPack.Sample/Pages/Index.cshtml index e6f6565..537d18e 100644 --- a/samples/TagHelperPack.Sample/Pages/Index.cshtml +++ b/samples/TagHelperPack.Sample/Pages/Index.cshtml @@ -327,8 +327,8 @@ public Customer Customer { get; set; }
This will only render if the user is authenticated.
This will only render when the user is *not* authenticated.
This will only render if the user is authenticated and authorized via the "AdminPolicy" policy.
-
This will only render if the user has "ViewUsers" permission.
-
This will only render if the user has "ManageUsers" permission.
+
This will only render if the user has "ViewUsers" permission.
+
This will only render if the user has "ManageUsers" permission.
This will only render if the user belongs to the "standard" role.
This will only render if the user belongs to the "admin" role.
@@ -338,8 +338,8 @@ public Customer Customer { get; set; }
<div asp-authz>This will only render if the user is authenticated.</div>
 <div asp-authz="false">This will only render when the user is <strong>*not*</strong> authenticated.</div>
 <div asp-authz-policy="AdminPolicy">This will only render if the user is authenticated and authorized via the "AdminPolicy" policy.</div>
-<div asp-authz-policy="PermissionPolicy" asp-authz-permission="ViewUsers">This will only render if the user has "ViewUsers" permission.</div>
-<div asp-authz-policy="PermissionPolicy" asp-authz-permission="ManageUsers">This will only render if the user has "ManageUsers" permission.</div>
+<div asp-authz-policy="PermissionPolicy" asp-authz-resource='"ViewUsers"'>This will only render if the user has "ViewUsers" permission.</div>
+<div asp-authz-policy="PermissionPolicy" asp-authz-resource='"ManageUsers"'>This will only render if the user has "ManageUsers" permission.</div>
 <div asp-authz-role="standard">This will only render if the user belongs to the "standard" role.</div>
 <div asp-authz-role="admin">This will only render if the user belongs to the "admin" role.</div>
diff --git a/src/TagHelperPack/AuthzTagHelper.cs b/src/TagHelperPack/AuthzTagHelper.cs index 7e55447..68cbd25 100644 --- a/src/TagHelperPack/AuthzTagHelper.cs +++ b/src/TagHelperPack/AuthzTagHelper.cs @@ -13,7 +13,7 @@ namespace TagHelperPack; [HtmlTargetElement("*", Attributes = AspAuthzAttributeName)] [HtmlTargetElement("*", Attributes = AspAuthzPolicyAttributeName)] [HtmlTargetElement("*", Attributes = AspAuthzRoleAttributeName)] -[HtmlTargetElement("*", Attributes = AspAuthzPolicyPermissionAttributeName)] +[HtmlTargetElement("*", Attributes = AspAuthzPolicyResourceAttributeName)] public class AuthzTagHelper : TagHelper { internal static object SuppressedKey = new(); @@ -22,7 +22,7 @@ public class AuthzTagHelper : TagHelper private const string AspAuthzAttributeName = "asp-authz"; private const string AspAuthzPolicyAttributeName = "asp-authz-policy"; private const string AspAuthzRoleAttributeName = "asp-authz-role"; - private const string AspAuthzPolicyPermissionAttributeName = "asp-authz-permission"; + private const string AspAuthzPolicyResourceAttributeName = "asp-authz-resource"; private readonly IAuthorizationService _authz; @@ -60,10 +60,10 @@ public AuthzTagHelper(IAuthorizationService authz) public string RequiredRole { get; set; } /// - /// A permission that must be satisfied in order for the current element to be rendered. asp-authz-policy should be set as well. + /// An optional resource used by the authorization policy handler. asp-authz-policy should be set as well. /// - [HtmlAttributeName(AspAuthzPolicyPermissionAttributeName)] - public string RequiredPolicyPermission { get; set; } + [HtmlAttributeName(AspAuthzPolicyResourceAttributeName)] + public object RequiredPolicyResource { get; set; } /// /// Gets or sets the . @@ -85,7 +85,12 @@ public override async Task ProcessAsync(TagHelperContext context, TagHelperOutpu throw new ArgumentNullException(nameof(output)); } - if(!string.IsNullOrEmpty(RequiredRole) && !string.IsNullOrEmpty(RequiredPolicy)) + if (RequiredPolicyResource != null && string.IsNullOrEmpty(RequiredPolicy)) + { + throw new ArgumentNullException(AspAuthzPolicyAttributeName); + } + + if (!string.IsNullOrEmpty(RequiredRole) && !string.IsNullOrEmpty(RequiredPolicy)) { throw new InvalidOperationException($"{AspAuthzRoleAttributeName} and {AspAuthzPolicyAttributeName} cannot be set at the same time."); } @@ -107,10 +112,10 @@ public override async Task ProcessAsync(TagHelperContext context, TagHelperOutpu else if (!string.IsNullOrEmpty(RequiredPolicy)) { bool authorized; - if (!string.IsNullOrEmpty(RequiredPolicyPermission)) + if (RequiredPolicyResource != null) { // auth-policy="foo" & user is authorized for policy "foo" based on permission - var cacheKey = AspAuthzPolicyPermissionAttributeName + "." + RequiredPolicyPermission; + var cacheKey = AspAuthzPolicyResourceAttributeName + "." + RequiredPolicyResource; var cachedResult = ViewContext.ViewData[cacheKey]; if (cachedResult != null) { @@ -118,7 +123,7 @@ public override async Task ProcessAsync(TagHelperContext context, TagHelperOutpu } else { - var authResult = await _authz.AuthorizeAsync(user, RequiredPolicyPermission, RequiredPolicy); + var authResult = await _authz.AuthorizeAsync(user, RequiredPolicyResource, RequiredPolicy); authorized = authResult.Succeeded; ViewContext.ViewData[cacheKey] = authorized; } From 60a82602b1d8a4172cc05a126f7da544662e366e Mon Sep 17 00:00:00 2001 From: MSingh-13 <106232578+MSingh-13@users.noreply.github.com> Date: Sat, 29 Mar 2025 02:10:07 +0300 Subject: [PATCH 7/9] Simplified HtmlHelperExtensions.MergeHtmlAttributes() --- src/TagHelperPack/HtmlHelperExtensions.cs | 37 ++++++----------------- 1 file changed, 9 insertions(+), 28 deletions(-) diff --git a/src/TagHelperPack/HtmlHelperExtensions.cs b/src/TagHelperPack/HtmlHelperExtensions.cs index 6c09f40..ffa5677 100644 --- a/src/TagHelperPack/HtmlHelperExtensions.cs +++ b/src/TagHelperPack/HtmlHelperExtensions.cs @@ -111,43 +111,24 @@ internal static IDictionary MergeHtmlAttributes(this IHtmlHelper var keysConcatValuesWithSpace = new string[] { "class" }; var keysConcatValuesWithSemiColon = new string[] { "style" }; - var htmlAttributesDict = newHtmlAttributesObject as IDictionary; - var defaultHtmlAttributesDict = existingHtmlAttributesObject as IDictionary; - - IDictionary htmlAttributes = (htmlAttributesDict != null) - ? new RouteValueDictionary(htmlAttributesDict) - : HtmlHelper.AnonymousObjectToHtmlAttributes(newHtmlAttributesObject); - - IDictionary existingHtmlAttributes = (defaultHtmlAttributesDict != null) - ? new RouteValueDictionary(defaultHtmlAttributesDict) - : HtmlHelper.AnonymousObjectToHtmlAttributes(existingHtmlAttributesObject); + IDictionary htmlAttributes = HtmlHelper.AnonymousObjectToHtmlAttributes(newHtmlAttributesObject); + IDictionary existingHtmlAttributes = HtmlHelper.AnonymousObjectToHtmlAttributes(existingHtmlAttributesObject); foreach (var item in htmlAttributes) { + string separator = string.Empty; if (keysConcatValuesWithSpace.Contains(item.Key)) { - existingHtmlAttributes.TryGetValue(item.Key, out object? value); - if (value != null && item.Value != null) - { - existingHtmlAttributes[item.Key] = value != null ? - string.Format("{0} {1}", existingHtmlAttributes[item.Key], item.Value) - : item.Value; - } + separator = " "; } else if (keysConcatValuesWithSemiColon.Contains(item.Key)) { - existingHtmlAttributes.TryGetValue(item.Key, out object? value); - if (value != null && item.Value != null) - { - existingHtmlAttributes[item.Key] = value != null ? - string.Format("{0}; {1}", existingHtmlAttributes[item.Key], item.Value) - : item.Value; - } - } - else - { - existingHtmlAttributes[item.Key] = item.Value; + separator = "; "; } + existingHtmlAttributes.TryGetValue(item.Key, out object? value); + existingHtmlAttributes[item.Key] = value != null && !string.IsNullOrEmpty(separator) ? + string.Format("{0}{1}{2}", existingHtmlAttributes[item.Key], separator, item.Value) + : item.Value; } return existingHtmlAttributes; From a107a8efffd3f4a7eb49010e50a8936b0e011002 Mon Sep 17 00:00:00 2001 From: MSingh-13 <106232578+MSingh-13@users.noreply.github.com> Date: Thu, 24 Apr 2025 22:42:49 +0300 Subject: [PATCH 8/9] Refactored MergeHtmlAttributes method. Further simplified the method. Correctly overwriting the values of attributes other than class and style. Renamed the method to MergeHtmlAttributesObjects Moved it to a new class PublicHtmlHelperExtensions so that it could be made public and used in view templates. --- src/TagHelperPack/DisplayTagHelper.cs | 2 +- src/TagHelperPack/EditorTagHelper.cs | 2 +- src/TagHelperPack/HtmlHelperExtensions.cs | 35 -------------- .../PublicHtmlHelperExtensions.cs | 48 +++++++++++++++++++ 4 files changed, 50 insertions(+), 37 deletions(-) create mode 100644 src/TagHelperPack/PublicHtmlHelperExtensions.cs diff --git a/src/TagHelperPack/DisplayTagHelper.cs b/src/TagHelperPack/DisplayTagHelper.cs index c946139..ecea3c5 100644 --- a/src/TagHelperPack/DisplayTagHelper.cs +++ b/src/TagHelperPack/DisplayTagHelper.cs @@ -95,7 +95,7 @@ public override void Process(TagHelperContext context, TagHelperOutput output) ((IViewContextAware)_htmlHelper).Contextualize(ViewContext); var cssViewData = new { htmlAttributes = new { @class = Class, style = Style } }; - var finalViewData = _htmlHelper.MergeHtmlAttributes(cssViewData, ViewData); + var finalViewData = _htmlHelper.MergeHtmlAttributesObjects(cssViewData, ViewData); output.Content.SetHtmlContent(_htmlHelper.Display(For, HtmlFieldName, TemplateName, finalViewData)); output.TagName = null; diff --git a/src/TagHelperPack/EditorTagHelper.cs b/src/TagHelperPack/EditorTagHelper.cs index 90cad0f..3afb969 100644 --- a/src/TagHelperPack/EditorTagHelper.cs +++ b/src/TagHelperPack/EditorTagHelper.cs @@ -95,7 +95,7 @@ public override void Process(TagHelperContext context, TagHelperOutput output) ((IViewContextAware)_htmlHelper).Contextualize(ViewContext); var cssViewData = new { htmlAttributes = new { @class = Class, style = Style } }; - var finalViewData = _htmlHelper.MergeHtmlAttributes(cssViewData, ViewData); + var finalViewData = _htmlHelper.MergeHtmlAttributesObjects(cssViewData, ViewData); output.Content.SetHtmlContent(_htmlHelper.Editor(For, HtmlFieldName, TemplateName, finalViewData)); output.TagName = null; diff --git a/src/TagHelperPack/HtmlHelperExtensions.cs b/src/TagHelperPack/HtmlHelperExtensions.cs index ffa5677..2b076c2 100644 --- a/src/TagHelperPack/HtmlHelperExtensions.cs +++ b/src/TagHelperPack/HtmlHelperExtensions.cs @@ -98,39 +98,4 @@ public static IHtmlContent Editor(this IHtmlHelper htmlHelper, ModelExpression m return htmlHelper.Editor(expression, templateName, htmlFieldName, additionalViewData); } - - /// - /// Merge values from 2 anonymous or IDictionary objects. Values of overlapping keys from the 'existing values' object are replaced by the values - /// from the 'new values' object except if the keys are 'class' or 'style', in which case the values are concatentated with a space or ; respectively. - /// - /// new values - /// existing values - /// - internal static IDictionary MergeHtmlAttributes(this IHtmlHelper helper, object newHtmlAttributesObject, object existingHtmlAttributesObject) - { - var keysConcatValuesWithSpace = new string[] { "class" }; - var keysConcatValuesWithSemiColon = new string[] { "style" }; - - IDictionary htmlAttributes = HtmlHelper.AnonymousObjectToHtmlAttributes(newHtmlAttributesObject); - IDictionary existingHtmlAttributes = HtmlHelper.AnonymousObjectToHtmlAttributes(existingHtmlAttributesObject); - - foreach (var item in htmlAttributes) - { - string separator = string.Empty; - if (keysConcatValuesWithSpace.Contains(item.Key)) - { - separator = " "; - } - else if (keysConcatValuesWithSemiColon.Contains(item.Key)) - { - separator = "; "; - } - existingHtmlAttributes.TryGetValue(item.Key, out object? value); - existingHtmlAttributes[item.Key] = value != null && !string.IsNullOrEmpty(separator) ? - string.Format("{0}{1}{2}", existingHtmlAttributes[item.Key], separator, item.Value) - : item.Value; - } - - return existingHtmlAttributes; - } } diff --git a/src/TagHelperPack/PublicHtmlHelperExtensions.cs b/src/TagHelperPack/PublicHtmlHelperExtensions.cs new file mode 100644 index 0000000..babefcd --- /dev/null +++ b/src/TagHelperPack/PublicHtmlHelperExtensions.cs @@ -0,0 +1,48 @@ +using Microsoft.AspNetCore.Html; +using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.AspNetCore.Routing; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using TagHelperPack; + +namespace Microsoft.AspNetCore.Mvc.ViewFeatures; + +public static class PublicHtmlHelperExtensions +{ + private static IDictionary htmlAttributesWithSeparators = new Dictionary() + { + { "class", " "}, + { "style", "; " }, + }; + + /// + /// Merge values from 2 anonymous or IDictionary objects. Values of overlapping keys from the 'existing values' object are replaced by the values + /// from the 'new values' object except if the keys are 'class' or 'style', in which case the values are concatentated with a space or ; respectively. + /// + /// new values + /// existing values + /// + public static IDictionary MergeHtmlAttributesObjects(this IHtmlHelper helper, object newHtmlAttributesObject, object existingHtmlAttributesObject) + { + IDictionary newHtmlAttributes = HtmlHelper.AnonymousObjectToHtmlAttributes(newHtmlAttributesObject); + IDictionary existingHtmlAttributes = HtmlHelper.AnonymousObjectToHtmlAttributes(existingHtmlAttributesObject); + + foreach (var item in newHtmlAttributes) + { + htmlAttributesWithSeparators.TryGetValue(item.Key, out string separator); + + existingHtmlAttributes.TryGetValue(item.Key, out object? value); + + if(item.Value != null) + { + existingHtmlAttributes[item.Key] = value != null && !string.IsNullOrEmpty(separator) ? + string.Format("{0}{1}{2}", existingHtmlAttributes[item.Key], separator, item.Value) + : item.Value; + } + } + + return existingHtmlAttributes; + } +} From c5428acc69f999cfa48afde3a3163d3f969a199e Mon Sep 17 00:00:00 2001 From: MSingh-13 <106232578+MSingh-13@users.noreply.github.com> Date: Fri, 25 Apr 2025 17:16:36 +0300 Subject: [PATCH 9/9] Updated DisplayTagHelper and EditorTagHelper. Switched from an anonymous object to dictionary for htmlAttributes, so that only properties that have values can be added to the dictionary. Added Id property to set the id on the html element. Updated samples to demonstrate the merging of class, style and id supplied with editor tag. Also demonstrates the use for view-data-htmAttributes. --- .../TagHelperPack.Sample/Pages/Index.cshtml | 15 +++++++- .../Shared/EditorTemplates/String3.cshtml | 18 ++++++++++ src/TagHelperPack/DisplayTagHelper.cs | 34 +++++++++++++++++-- src/TagHelperPack/EditorTagHelper.cs | 34 +++++++++++++++++-- 4 files changed, 94 insertions(+), 7 deletions(-) create mode 100644 samples/TagHelperPack.Sample/Views/Shared/EditorTemplates/String3.cshtml diff --git a/samples/TagHelperPack.Sample/Pages/Index.cshtml b/samples/TagHelperPack.Sample/Pages/Index.cshtml index 537d18e..fa3933a 100644 --- a/samples/TagHelperPack.Sample/Pages/Index.cshtml +++ b/samples/TagHelperPack.Sample/Pages/Index.cshtml @@ -42,6 +42,9 @@
  • Use the template-name attribute to override the template used to render the model.
  • Use the html-field-name attribute to override the HTML field name used to render the model.
  • Use view-data-* and view-data attributes to provide additional ViewData to the template.
  • +
  • Use view-data-htmlAttributes attribute to add properties to the htmlAttributes property in ViewData. This property can then be used as an argument to Html helpers in the template to render it in the final html.
  • +
  • Use class and style attributes to add additional css classes or inline styles to the htmlAttributes property in ViewData.
  • +
  • Use id attribute to add the id property to the htmlAttributes property in ViewData.
  • @@ -51,7 +54,7 @@

  • Use the asp-template-name attribute to override the template used to render the model.
  • Use the asp-html-field-name attribute to override the HTML field name used to render the model.
  • Use asp-view-data-* and asp-view-data attributes to provide additional ViewData to the template.
  • - +

    Use <datalist asp-list="..."> to render a <datalist> element containing <option> elements @@ -79,6 +82,11 @@ +

    + + + +
    @@ -143,6 +151,11 @@ <editor for="LastName" template-name="String2" /> <span asp-validation-for="LastName" class="text-danger"></span> </div> + <div class="form-group"> + <label asp-for="LastName"></label> + <editor for="LastName" template-name="String3" id="myId" class="myClass" style="color: green" view-data-htmlAttributes='new {data_info = "some info"}' /> + <span asp-validation-for="LastName" class="text-danger"></span> + </div> <div class="form-group"> <label asp-display-name-for="LastName" for="CustomerLastName"></label> <editor for="LastName" html-field-name="CustomerLastName" /> diff --git a/samples/TagHelperPack.Sample/Views/Shared/EditorTemplates/String3.cshtml b/samples/TagHelperPack.Sample/Views/Shared/EditorTemplates/String3.cshtml new file mode 100644 index 0000000..1b9fcb4 --- /dev/null +++ b/samples/TagHelperPack.Sample/Views/Shared/EditorTemplates/String3.cshtml @@ -0,0 +1,18 @@ +@model string + +@using Microsoft.AspNetCore.Mvc.ViewFeatures; + +@{ + //default html attributes added by the template + var defaultHtmlAttributesObject = new + { + Class = "form-control", + style= "font-weight: bold" + }; + + //merging the above html attributes with attributes received from the view + var viewHtmlAttributes = ViewData["htmlAttributes"] ?? new { }; + var mergedHtmlAttributes = Html.MergeHtmlAttributesObjects(viewHtmlAttributes, defaultHtmlAttributesObject); +} + +@Html.TextBox("", Model, mergedHtmlAttributes) diff --git a/src/TagHelperPack/DisplayTagHelper.cs b/src/TagHelperPack/DisplayTagHelper.cs index ecea3c5..7b8ee33 100644 --- a/src/TagHelperPack/DisplayTagHelper.cs +++ b/src/TagHelperPack/DisplayTagHelper.cs @@ -74,6 +74,12 @@ public IDictionary ViewData [HtmlAttributeName("style")] public string Style { get; set; } + /// + /// HTML id. + /// + [HtmlAttributeName("id")] + public string Id { get; set; } + /// public override void Process(TagHelperContext context, TagHelperOutput output) { @@ -94,9 +100,31 @@ public override void Process(TagHelperContext context, TagHelperOutput output) ((IViewContextAware)_htmlHelper).Contextualize(ViewContext); - var cssViewData = new { htmlAttributes = new { @class = Class, style = Style } }; - var finalViewData = _htmlHelper.MergeHtmlAttributesObjects(cssViewData, ViewData); - output.Content.SetHtmlContent(_htmlHelper.Display(For, HtmlFieldName, TemplateName, finalViewData)); + //create a local htmlAttributes dictionary for html attributes exposed by the tag helper + IDictionary htmlAttributes = new Dictionary(); + if (!string.IsNullOrWhiteSpace(Id)) + { + htmlAttributes["id"] = Id; + } + if (!string.IsNullOrWhiteSpace(Class)) + { + htmlAttributes["class"] = Class; + } + if (!string.IsNullOrWhiteSpace(Style)) + { + htmlAttributes["style"] = Style; + } + + //get the htmlAttributes property from tag ViewData + ViewData.TryGetValue("htmlAttributes", out var viewDataHtmlAttributes); + + //merging local and ViewData htmlAttributes properties + htmlAttributes = _htmlHelper.MergeHtmlAttributesObjects(htmlAttributes, viewDataHtmlAttributes); + + //setting the merged htmlAttributes on the ViewData of the tag. + ViewData["htmlAttributes"] = htmlAttributes; + + output.Content.SetHtmlContent(_htmlHelper.Display(For, HtmlFieldName, TemplateName, ViewData)); output.TagName = null; } diff --git a/src/TagHelperPack/EditorTagHelper.cs b/src/TagHelperPack/EditorTagHelper.cs index 3afb969..07c4878 100644 --- a/src/TagHelperPack/EditorTagHelper.cs +++ b/src/TagHelperPack/EditorTagHelper.cs @@ -74,6 +74,12 @@ public IDictionary ViewData [HtmlAttributeName("style")] public string Style { get; set; } + /// + /// HTML id. + /// + [HtmlAttributeName("id")] + public string Id { get; set; } + /// public override void Process(TagHelperContext context, TagHelperOutput output) { @@ -94,9 +100,31 @@ public override void Process(TagHelperContext context, TagHelperOutput output) ((IViewContextAware)_htmlHelper).Contextualize(ViewContext); - var cssViewData = new { htmlAttributes = new { @class = Class, style = Style } }; - var finalViewData = _htmlHelper.MergeHtmlAttributesObjects(cssViewData, ViewData); - output.Content.SetHtmlContent(_htmlHelper.Editor(For, HtmlFieldName, TemplateName, finalViewData)); + //create a local htmlAttributes dictionary for html attributes exposed by the tag helper + IDictionary htmlAttributes = new Dictionary(); + if (!string.IsNullOrWhiteSpace(Id)) + { + htmlAttributes["id"] = Id; + } + if (!string.IsNullOrWhiteSpace(Class)) + { + htmlAttributes["class"] = Class; + } + if (!string.IsNullOrWhiteSpace(Style)) + { + htmlAttributes["style"] = Style; + } + + //get the htmlAttributes property from tag ViewData + ViewData.TryGetValue("htmlAttributes", out var viewDataHtmlAttributes); + + //merging local and ViewData htmlAttributes properties + htmlAttributes = _htmlHelper.MergeHtmlAttributesObjects(htmlAttributes, viewDataHtmlAttributes); + + //setting the merged htmlAttributes on the ViewData of the tag. + ViewData["htmlAttributes"] = htmlAttributes; + + output.Content.SetHtmlContent(_htmlHelper.Editor(For, HtmlFieldName, TemplateName, ViewData)); output.TagName = null; }