From 1bb3ca1f3b1fcc8e238e188ab6fbaa32fef3bd05 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 29 Jul 2026 09:51:59 +0000 Subject: [PATCH 1/2] Fix proxy generator to read roles from [Roles("...")] constructor form (#2381) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The role extraction only inspected named arguments, so the idiomatic [Roles("...")] attribute — which sets Roles via a params string[] constructor — yielded no roles. Also read the constructor argument array so both the constructor and named-argument forms produce roles. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01B1mrPcRT2GYjRxrx4T1p8N --- .../RoleSecuredTypes.cs | 23 +++++++++++++++++++ .../from_both_forms_combined.cs | 18 +++++++++++++++ ...om_constructor_form_with_multiple_roles.cs | 18 +++++++++++++++ .../from_constructor_form_with_single_role.cs | 18 +++++++++++++++ .../from_named_argument_form.cs | 18 +++++++++++++++ .../ProxyGenerator/MethodInfoExtensions.cs | 11 +++++++++ 6 files changed, 106 insertions(+) create mode 100644 Source/DotNET/Tools/ProxyGenerator.Specs/ControllerBased/for_MethodInfoExtensions/RoleSecuredTypes.cs create mode 100644 Source/DotNET/Tools/ProxyGenerator.Specs/ControllerBased/for_MethodInfoExtensions/when_extracting_roles/from_both_forms_combined.cs create mode 100644 Source/DotNET/Tools/ProxyGenerator.Specs/ControllerBased/for_MethodInfoExtensions/when_extracting_roles/from_constructor_form_with_multiple_roles.cs create mode 100644 Source/DotNET/Tools/ProxyGenerator.Specs/ControllerBased/for_MethodInfoExtensions/when_extracting_roles/from_constructor_form_with_single_role.cs create mode 100644 Source/DotNET/Tools/ProxyGenerator.Specs/ControllerBased/for_MethodInfoExtensions/when_extracting_roles/from_named_argument_form.cs diff --git a/Source/DotNET/Tools/ProxyGenerator.Specs/ControllerBased/for_MethodInfoExtensions/RoleSecuredTypes.cs b/Source/DotNET/Tools/ProxyGenerator.Specs/ControllerBased/for_MethodInfoExtensions/RoleSecuredTypes.cs new file mode 100644 index 000000000..f35713840 --- /dev/null +++ b/Source/DotNET/Tools/ProxyGenerator.Specs/ControllerBased/for_MethodInfoExtensions/RoleSecuredTypes.cs @@ -0,0 +1,23 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Cratis.Arc.Authorization; +using Microsoft.AspNetCore.Authorization; + +namespace Cratis.Arc.ProxyGenerator.ControllerBased.for_MethodInfoExtensions; + +public class RoleSecuredTypes +{ + [Roles("Librarian")] + public void SingleRoleFromConstructor() { } + + [Roles("Librarian", "Admin")] + public void MultipleRolesFromConstructor() { } + + [Authorize(Roles = "Librarian")] + public void RoleFromNamedArgument() { } + + [Roles("Librarian")] + [Authorize(Roles = "Librarian")] + public void RoleFromBothForms() { } +} diff --git a/Source/DotNET/Tools/ProxyGenerator.Specs/ControllerBased/for_MethodInfoExtensions/when_extracting_roles/from_both_forms_combined.cs b/Source/DotNET/Tools/ProxyGenerator.Specs/ControllerBased/for_MethodInfoExtensions/when_extracting_roles/from_both_forms_combined.cs new file mode 100644 index 000000000..46d7b4b47 --- /dev/null +++ b/Source/DotNET/Tools/ProxyGenerator.Specs/ControllerBased/for_MethodInfoExtensions/when_extracting_roles/from_both_forms_combined.cs @@ -0,0 +1,18 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Reflection; + +namespace Cratis.Arc.ProxyGenerator.ControllerBased.for_MethodInfoExtensions.when_extracting_roles; + +public class from_both_forms_combined : Specification +{ + MethodInfo _method; + IEnumerable _result; + + void Establish() => _method = typeof(RoleSecuredTypes).GetMethod(nameof(RoleSecuredTypes.RoleFromBothForms)); + + void Because() => _result = _method.GetRoles(); + + [Fact] void should_deduplicate_the_roles() => _result.ShouldContainOnly(["Librarian"]); +} diff --git a/Source/DotNET/Tools/ProxyGenerator.Specs/ControllerBased/for_MethodInfoExtensions/when_extracting_roles/from_constructor_form_with_multiple_roles.cs b/Source/DotNET/Tools/ProxyGenerator.Specs/ControllerBased/for_MethodInfoExtensions/when_extracting_roles/from_constructor_form_with_multiple_roles.cs new file mode 100644 index 000000000..a539b2c75 --- /dev/null +++ b/Source/DotNET/Tools/ProxyGenerator.Specs/ControllerBased/for_MethodInfoExtensions/when_extracting_roles/from_constructor_form_with_multiple_roles.cs @@ -0,0 +1,18 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Reflection; + +namespace Cratis.Arc.ProxyGenerator.ControllerBased.for_MethodInfoExtensions.when_extracting_roles; + +public class from_constructor_form_with_multiple_roles : Specification +{ + MethodInfo _method; + IEnumerable _result; + + void Establish() => _method = typeof(RoleSecuredTypes).GetMethod(nameof(RoleSecuredTypes.MultipleRolesFromConstructor)); + + void Because() => _result = _method.GetRoles(); + + [Fact] void should_yield_all_roles() => _result.ShouldContainOnly(["Librarian", "Admin"]); +} diff --git a/Source/DotNET/Tools/ProxyGenerator.Specs/ControllerBased/for_MethodInfoExtensions/when_extracting_roles/from_constructor_form_with_single_role.cs b/Source/DotNET/Tools/ProxyGenerator.Specs/ControllerBased/for_MethodInfoExtensions/when_extracting_roles/from_constructor_form_with_single_role.cs new file mode 100644 index 000000000..fcfaf30df --- /dev/null +++ b/Source/DotNET/Tools/ProxyGenerator.Specs/ControllerBased/for_MethodInfoExtensions/when_extracting_roles/from_constructor_form_with_single_role.cs @@ -0,0 +1,18 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Reflection; + +namespace Cratis.Arc.ProxyGenerator.ControllerBased.for_MethodInfoExtensions.when_extracting_roles; + +public class from_constructor_form_with_single_role : Specification +{ + MethodInfo _method; + IEnumerable _result; + + void Establish() => _method = typeof(RoleSecuredTypes).GetMethod(nameof(RoleSecuredTypes.SingleRoleFromConstructor)); + + void Because() => _result = _method.GetRoles(); + + [Fact] void should_yield_the_role() => _result.ShouldContainOnly(["Librarian"]); +} diff --git a/Source/DotNET/Tools/ProxyGenerator.Specs/ControllerBased/for_MethodInfoExtensions/when_extracting_roles/from_named_argument_form.cs b/Source/DotNET/Tools/ProxyGenerator.Specs/ControllerBased/for_MethodInfoExtensions/when_extracting_roles/from_named_argument_form.cs new file mode 100644 index 000000000..0649e53bd --- /dev/null +++ b/Source/DotNET/Tools/ProxyGenerator.Specs/ControllerBased/for_MethodInfoExtensions/when_extracting_roles/from_named_argument_form.cs @@ -0,0 +1,18 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Reflection; + +namespace Cratis.Arc.ProxyGenerator.ControllerBased.for_MethodInfoExtensions.when_extracting_roles; + +public class from_named_argument_form : Specification +{ + MethodInfo _method; + IEnumerable _result; + + void Establish() => _method = typeof(RoleSecuredTypes).GetMethod(nameof(RoleSecuredTypes.RoleFromNamedArgument)); + + void Because() => _result = _method.GetRoles(); + + [Fact] void should_yield_the_role() => _result.ShouldContainOnly(["Librarian"]); +} diff --git a/Source/DotNET/Tools/ProxyGenerator/MethodInfoExtensions.cs b/Source/DotNET/Tools/ProxyGenerator/MethodInfoExtensions.cs index b624143c4..a46588e50 100644 --- a/Source/DotNET/Tools/ProxyGenerator/MethodInfoExtensions.cs +++ b/Source/DotNET/Tools/ProxyGenerator/MethodInfoExtensions.cs @@ -105,6 +105,7 @@ static IEnumerable GetRolesFromAttributesData(IEnumerable a.MemberName == "Roles"); if (rolesArg != default && rolesArg.TypedValue.Value is string rolesStr && !string.IsNullOrEmpty(rolesStr)) { @@ -113,6 +114,16 @@ static IEnumerable GetRolesFromAttributesData(IEnumerable 0 && + attr.ConstructorArguments[0].Value is IReadOnlyCollection roleArgs) + { + foreach (var role in roleArgs.Select(a => a.Value as string).Where(r => !string.IsNullOrEmpty(r))) + { + yield return role!; + } + } } } From d475a295ff822c8b878e0312173c656d3635b965 Mon Sep 17 00:00:00 2001 From: woksin Date: Wed, 29 Jul 2026 13:42:15 +0200 Subject: [PATCH 2/2] Name the Authorize attribute the fixture means Cratis.Arc.Authorization and Microsoft.AspNetCore.Authorization both declare an Authorize attribute, so importing both left every use of it ambiguous and the fixture did not compile. The named-argument form is the ASP.NET Core one, which is now written out in full. Co-Authored-By: Claude Opus 5 --- .../for_MethodInfoExtensions/RoleSecuredTypes.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/Source/DotNET/Tools/ProxyGenerator.Specs/ControllerBased/for_MethodInfoExtensions/RoleSecuredTypes.cs b/Source/DotNET/Tools/ProxyGenerator.Specs/ControllerBased/for_MethodInfoExtensions/RoleSecuredTypes.cs index f35713840..a48ad4770 100644 --- a/Source/DotNET/Tools/ProxyGenerator.Specs/ControllerBased/for_MethodInfoExtensions/RoleSecuredTypes.cs +++ b/Source/DotNET/Tools/ProxyGenerator.Specs/ControllerBased/for_MethodInfoExtensions/RoleSecuredTypes.cs @@ -2,10 +2,16 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using Cratis.Arc.Authorization; -using Microsoft.AspNetCore.Authorization; namespace Cratis.Arc.ProxyGenerator.ControllerBased.for_MethodInfoExtensions; +/// +/// Holds the shapes roles can be declared in, for reading them back. +/// +/// +/// Both Cratis.Arc.Authorization and Microsoft.AspNetCore.Authorization declare an +/// Authorize attribute, so the ASP.NET Core one is written out in full where the named-argument form is needed. +/// public class RoleSecuredTypes { [Roles("Librarian")] @@ -14,10 +20,10 @@ public void SingleRoleFromConstructor() { } [Roles("Librarian", "Admin")] public void MultipleRolesFromConstructor() { } - [Authorize(Roles = "Librarian")] + [Microsoft.AspNetCore.Authorization.Authorize(Roles = "Librarian")] public void RoleFromNamedArgument() { } [Roles("Librarian")] - [Authorize(Roles = "Librarian")] + [Microsoft.AspNetCore.Authorization.Authorize(Roles = "Librarian")] public void RoleFromBothForms() { } }