From ff7e77256ebc3a3b303981d631b751485b278087 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 21 May 2026 14:37:49 +0200 Subject: [PATCH 01/25] Use resource designer Type in ResourceIdManager Emit ResourceDesignerAttribute with typeof(Resource) for app resource designers so ResourceIdManager can use the attribute-provided Type instead of Assembly.GetType(). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Android.Runtime/ResourceDesignerAttribute.cs | 15 +++++++++++++-- .../Android.Runtime/ResourceIdManager.cs | 12 +----------- .../PublicAPI/API-35/PublicAPI.Unshipped.txt | 3 +++ .../PublicAPI/API-36.1/PublicAPI.Unshipped.txt | 3 +++ .../PublicAPI/API-36/PublicAPI.Unshipped.txt | 3 +++ .../PublicAPI/API-37/PublicAPI.Unshipped.txt | 3 +++ .../Tasks/GenerateResourceDesigner.cs | 5 ++++- .../Expected/GenerateDesignerFileExpected.cs | 2 +- ...erFileWithElevenStyleableAttributesExpected.cs | 2 +- ...ateDesignerFileWithLibraryReferenceExpected.cs | 2 +- .../Utilities/ResourceDesignerImportGenerator.cs | 5 ++--- 11 files changed, 35 insertions(+), 20 deletions(-) diff --git a/src/Mono.Android/Android.Runtime/ResourceDesignerAttribute.cs b/src/Mono.Android/Android.Runtime/ResourceDesignerAttribute.cs index 9528a5097e8..9521838f911 100644 --- a/src/Mono.Android/Android.Runtime/ResourceDesignerAttribute.cs +++ b/src/Mono.Android/Android.Runtime/ResourceDesignerAttribute.cs @@ -8,14 +8,25 @@ public class ResourceDesignerAttribute : Attribute { public ResourceDesignerAttribute ( [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicMethods)] - string fullName) + Type resourceType) + { + if (resourceType == null) + throw new ArgumentNullException (nameof (resourceType)); + + ResourceType = resourceType; + FullName = resourceType.FullName ?? resourceType.Name; + } + + public ResourceDesignerAttribute (string fullName) { FullName = fullName; } - [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicMethods)] public string FullName { get; set; } + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicMethods)] + public Type? ResourceType { get; set; } + public bool IsApplication { get; set; } } } diff --git a/src/Mono.Android/Android.Runtime/ResourceIdManager.cs b/src/Mono.Android/Android.Runtime/ResourceIdManager.cs index 5ef1e6255df..2886513f91c 100644 --- a/src/Mono.Android/Android.Runtime/ResourceIdManager.cs +++ b/src/Mono.Android/Android.Runtime/ResourceIdManager.cs @@ -34,22 +34,12 @@ public static void UpdateIdValues () [return: DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicMethods)] static Type? GetResourceTypeFromAssembly (Assembly assembly) { - const string rootAssembly = "Resources.UpdateIdValues() methods are trimmed away by the LinkResourceDesigner trimmer step. This codepath is not called unless $(AndroidUseDesignerAssembly) is disabled."; - - [UnconditionalSuppressMessage ("Trimming", "IL2026", Justification = rootAssembly)] - [UnconditionalSuppressMessage ("Trimming", "IL2073", Justification = rootAssembly)] - [return: DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicMethods)] - static Type AssemblyGetType (Assembly a, string name) => a.GetType (name); - foreach (var customAttribute in assembly.GetCustomAttributes (typeof (ResourceDesignerAttribute), true)) { if (customAttribute is ResourceDesignerAttribute resourceDesignerAttribute && resourceDesignerAttribute.IsApplication) { - var type = AssemblyGetType (assembly, resourceDesignerAttribute.FullName); - if (type != null) - return type; + return resourceDesignerAttribute.ResourceType; } } return null; } } } - diff --git a/src/Mono.Android/PublicAPI/API-35/PublicAPI.Unshipped.txt b/src/Mono.Android/PublicAPI/API-35/PublicAPI.Unshipped.txt index 4f7e6a01587..ed6a6d1254c 100644 --- a/src/Mono.Android/PublicAPI/API-35/PublicAPI.Unshipped.txt +++ b/src/Mono.Android/PublicAPI/API-35/PublicAPI.Unshipped.txt @@ -1,3 +1,6 @@ #nullable enable Android.App.ApplicationAttribute.EnableOnBackInvokedCallback.get -> bool Android.App.ApplicationAttribute.EnableOnBackInvokedCallback.set -> void +Android.Runtime.ResourceDesignerAttribute.ResourceDesignerAttribute(System.Type! resourceType) -> void +Android.Runtime.ResourceDesignerAttribute.ResourceType.get -> System.Type? +Android.Runtime.ResourceDesignerAttribute.ResourceType.set -> void diff --git a/src/Mono.Android/PublicAPI/API-36.1/PublicAPI.Unshipped.txt b/src/Mono.Android/PublicAPI/API-36.1/PublicAPI.Unshipped.txt index 4f7e6a01587..ed6a6d1254c 100644 --- a/src/Mono.Android/PublicAPI/API-36.1/PublicAPI.Unshipped.txt +++ b/src/Mono.Android/PublicAPI/API-36.1/PublicAPI.Unshipped.txt @@ -1,3 +1,6 @@ #nullable enable Android.App.ApplicationAttribute.EnableOnBackInvokedCallback.get -> bool Android.App.ApplicationAttribute.EnableOnBackInvokedCallback.set -> void +Android.Runtime.ResourceDesignerAttribute.ResourceDesignerAttribute(System.Type! resourceType) -> void +Android.Runtime.ResourceDesignerAttribute.ResourceType.get -> System.Type? +Android.Runtime.ResourceDesignerAttribute.ResourceType.set -> void diff --git a/src/Mono.Android/PublicAPI/API-36/PublicAPI.Unshipped.txt b/src/Mono.Android/PublicAPI/API-36/PublicAPI.Unshipped.txt index 7d138d0c17f..cdf12e0bbdd 100644 --- a/src/Mono.Android/PublicAPI/API-36/PublicAPI.Unshipped.txt +++ b/src/Mono.Android/PublicAPI/API-36/PublicAPI.Unshipped.txt @@ -1972,6 +1972,9 @@ Android.Ranging.Wifi.Rtt.RttRangingParams.IsPeriodicRangingHwFeatureEnabled.get Android.Ranging.Wifi.Rtt.RttRangingParams.RangingUpdateRate.get -> Android.Ranging.Raw.RangingDeviceUpdateRate Android.Ranging.Wifi.Rtt.RttRangingParams.ServiceName.get -> string! Android.Ranging.Wifi.Rtt.RttRangingParams.WriteToParcel(Android.OS.Parcel! dest, Android.OS.ParcelableWriteFlags flags) -> void +Android.Runtime.ResourceDesignerAttribute.ResourceDesignerAttribute(System.Type! resourceType) -> void +Android.Runtime.ResourceDesignerAttribute.ResourceType.get -> System.Type? +Android.Runtime.ResourceDesignerAttribute.ResourceType.set -> void Android.Security.AdvancedProtection.AdvancedProtectionManager Android.Security.AdvancedProtection.AdvancedProtectionManager.ICallback Android.Security.AdvancedProtection.AdvancedProtectionManager.ICallback.OnAdvancedProtectionChanged(bool enabled) -> void diff --git a/src/Mono.Android/PublicAPI/API-37/PublicAPI.Unshipped.txt b/src/Mono.Android/PublicAPI/API-37/PublicAPI.Unshipped.txt index 4f7e6a01587..ed6a6d1254c 100644 --- a/src/Mono.Android/PublicAPI/API-37/PublicAPI.Unshipped.txt +++ b/src/Mono.Android/PublicAPI/API-37/PublicAPI.Unshipped.txt @@ -1,3 +1,6 @@ #nullable enable Android.App.ApplicationAttribute.EnableOnBackInvokedCallback.get -> bool Android.App.ApplicationAttribute.EnableOnBackInvokedCallback.set -> void +Android.Runtime.ResourceDesignerAttribute.ResourceDesignerAttribute(System.Type! resourceType) -> void +Android.Runtime.ResourceDesignerAttribute.ResourceType.get -> System.Type? +Android.Runtime.ResourceDesignerAttribute.ResourceType.set -> void diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateResourceDesigner.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateResourceDesigner.cs index 4fb35a3c26b..235d0ec417e 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateResourceDesigner.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateResourceDesigner.cs @@ -201,7 +201,10 @@ private void WriteFile (string file, CodeTypeDeclaration resources, string langu unit.Namespaces.Add (ns); var resgenatt = new CodeAttributeDeclaration (new CodeTypeReference ("Android.Runtime.ResourceDesignerAttribute", CodeTypeReferenceOptions.GlobalReference)); - resgenatt.Arguments.Add (new CodeAttributeArgument (new CodePrimitiveExpression (namespaceName.Length > 0 ? namespaceName + ".Resource" : "Resource"))); + var resourceType = new CodeTypeReference (namespaceName.Length > 0 ? namespaceName + ".Resource" : "Resource", CodeTypeReferenceOptions.GlobalReference); + resgenatt.Arguments.Add (new CodeAttributeArgument (IsApplication + ? new CodeTypeOfExpression (resourceType) + : new CodePrimitiveExpression (resourceType.BaseType))); resgenatt.Arguments.Add (new CodeAttributeArgument ("IsApplication", new CodePrimitiveExpression (IsApplication))); unit.AssemblyCustomAttributes.Add (resgenatt); diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Expected/GenerateDesignerFileExpected.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Expected/GenerateDesignerFileExpected.cs index 6fd85d7ee77..fd2065e455e 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Expected/GenerateDesignerFileExpected.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Expected/GenerateDesignerFileExpected.cs @@ -8,7 +8,7 @@ // //------------------------------------------------------------------------------ -[assembly: global::Android.Runtime.ResourceDesignerAttribute("Foo.Foo.Resource", IsApplication=true)] +[assembly: global::Android.Runtime.ResourceDesignerAttribute(typeof(global::Foo.Foo.Resource), IsApplication=true)] namespace Foo.Foo { diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Expected/GenerateDesignerFileWithElevenStyleableAttributesExpected.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Expected/GenerateDesignerFileWithElevenStyleableAttributesExpected.cs index 3b3fd726277..1dcd3ef462b 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Expected/GenerateDesignerFileWithElevenStyleableAttributesExpected.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Expected/GenerateDesignerFileWithElevenStyleableAttributesExpected.cs @@ -9,7 +9,7 @@ // //------------------------------------------------------------------------------ -[assembly: global::Android.Runtime.ResourceDesignerAttribute("Foo.Foo.Resource", IsApplication=true)] +[assembly: global::Android.Runtime.ResourceDesignerAttribute(typeof(global::Foo.Foo.Resource), IsApplication=true)] namespace Foo.Foo { diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Expected/GenerateDesignerFileWithLibraryReferenceExpected.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Expected/GenerateDesignerFileWithLibraryReferenceExpected.cs index 70688414260..921a4eed30e 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Expected/GenerateDesignerFileWithLibraryReferenceExpected.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Expected/GenerateDesignerFileWithLibraryReferenceExpected.cs @@ -8,7 +8,7 @@ // //------------------------------------------------------------------------------ -[assembly: global::Android.Runtime.ResourceDesignerAttribute("Foo.Foo.Resource", IsApplication=true)] +[assembly: global::Android.Runtime.ResourceDesignerAttribute(typeof(global::Foo.Foo.Resource), IsApplication=true)] namespace Foo.Foo { diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/ResourceDesignerImportGenerator.cs b/src/Xamarin.Android.Build.Tasks/Utilities/ResourceDesignerImportGenerator.cs index 9a5a0c1df29..da6853f6239 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/ResourceDesignerImportGenerator.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/ResourceDesignerImportGenerator.cs @@ -83,8 +83,8 @@ public void CreateImportMethods (IEnumerable libraries) string? GetResourceDesignerClass (MetadataReader reader) { - // Looking for: - // [assembly: Android.Runtime.ResourceDesignerAttribute("MyApp.Resource", IsApplication=true)] + // Looking for library assemblies: + // [assembly: Android.Runtime.ResourceDesignerAttribute("MyLibrary.Resource", IsApplication=false)] var assembly = reader.GetAssemblyDefinition (); foreach (var handle in assembly.GetCustomAttributes ()) { @@ -127,4 +127,3 @@ void CreateImportFor (string declaringTypeFullName, TypeDefinition type, CodeMem } } } - From 4430cbf701c0be4e0df5942abf4864b533bf5780 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 21 May 2026 15:15:57 +0200 Subject: [PATCH 02/25] Keep resource designer string attribute compatible Mark the legacy string-based ResourceDesignerAttribute constructor as requiring dynamic code, and emit the Type-based constructor from generated resource designers so internal code avoids the dynamic-code path. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Mono.Android/Android.Runtime/ResourceDesignerAttribute.cs | 3 +++ .../Tasks/GenerateResourceDesigner.cs | 4 +--- .../Utilities/DummyCustomAttributeProvider.cs | 2 +- .../Utilities/ResourceDesignerImportGenerator.cs | 3 ++- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/Mono.Android/Android.Runtime/ResourceDesignerAttribute.cs b/src/Mono.Android/Android.Runtime/ResourceDesignerAttribute.cs index 9521838f911..1759fafdcd8 100644 --- a/src/Mono.Android/Android.Runtime/ResourceDesignerAttribute.cs +++ b/src/Mono.Android/Android.Runtime/ResourceDesignerAttribute.cs @@ -6,6 +6,8 @@ namespace Android.Runtime [AttributeUsage (AttributeTargets.Assembly)] public class ResourceDesignerAttribute : Attribute { + const string UseResourceTypeConstructor = "Resource designer lookup by name requires dynamic code. Use ResourceDesignerAttribute(Type) instead."; + public ResourceDesignerAttribute ( [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicMethods)] Type resourceType) @@ -17,6 +19,7 @@ public ResourceDesignerAttribute ( FullName = resourceType.FullName ?? resourceType.Name; } + [RequiresDynamicCode (UseResourceTypeConstructor)] public ResourceDesignerAttribute (string fullName) { FullName = fullName; diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateResourceDesigner.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateResourceDesigner.cs index 235d0ec417e..ee3125e7b2f 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateResourceDesigner.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateResourceDesigner.cs @@ -202,9 +202,7 @@ private void WriteFile (string file, CodeTypeDeclaration resources, string langu var resgenatt = new CodeAttributeDeclaration (new CodeTypeReference ("Android.Runtime.ResourceDesignerAttribute", CodeTypeReferenceOptions.GlobalReference)); var resourceType = new CodeTypeReference (namespaceName.Length > 0 ? namespaceName + ".Resource" : "Resource", CodeTypeReferenceOptions.GlobalReference); - resgenatt.Arguments.Add (new CodeAttributeArgument (IsApplication - ? new CodeTypeOfExpression (resourceType) - : new CodePrimitiveExpression (resourceType.BaseType))); + resgenatt.Arguments.Add (new CodeAttributeArgument (new CodeTypeOfExpression (resourceType))); resgenatt.Arguments.Add (new CodeAttributeArgument ("IsApplication", new CodePrimitiveExpression (IsApplication))); unit.AssemblyCustomAttributes.Add (resgenatt); diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/DummyCustomAttributeProvider.cs b/src/Xamarin.Android.Build.Tasks/Utilities/DummyCustomAttributeProvider.cs index a4d5869c42c..7489de33838 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/DummyCustomAttributeProvider.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/DummyCustomAttributeProvider.cs @@ -27,7 +27,7 @@ public class DummyCustomAttributeProvider : ICustomAttributeTypeProvider null; - public object? GetTypeFromSerializedName (string name) => null; + public object? GetTypeFromSerializedName (string name) => name; public PrimitiveTypeCode GetUnderlyingEnumType (object? type) => default (PrimitiveTypeCode); diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/ResourceDesignerImportGenerator.cs b/src/Xamarin.Android.Build.Tasks/Utilities/ResourceDesignerImportGenerator.cs index da6853f6239..d86fb3fadf4 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/ResourceDesignerImportGenerator.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/ResourceDesignerImportGenerator.cs @@ -84,6 +84,7 @@ public void CreateImportMethods (IEnumerable libraries) string? GetResourceDesignerClass (MetadataReader reader) { // Looking for library assemblies: + // [assembly: Android.Runtime.ResourceDesignerAttribute(typeof(MyLibrary.Resource), IsApplication=false)] // [assembly: Android.Runtime.ResourceDesignerAttribute("MyLibrary.Resource", IsApplication=false)] var assembly = reader.GetAssemblyDefinition (); @@ -98,7 +99,7 @@ public void CreateImportMethods (IEnumerable libraries) return null; } } - return (string?) values.FixedArguments.First ().Value; + return values.FixedArguments.First ().Value as string; } } return null; From 55ca233b821b5b5077193bd752329c60110c79fe Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 21 May 2026 15:19:27 +0200 Subject: [PATCH 03/25] Move resource designer type lookup into attribute Keep the legacy string constructor behavior in ResourceDesignerAttribute, but route it through a dynamic-code-marked constructor and a localized Assembly.GetType fallback. The Type constructor remains the safe path used by generated resource designers. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../ResourceDesignerAttribute.cs | 23 +++++++++++++++++-- .../Android.Runtime/ResourceIdManager.cs | 2 +- .../PublicAPI/API-35/PublicAPI.Unshipped.txt | 2 -- .../API-36.1/PublicAPI.Unshipped.txt | 2 -- .../PublicAPI/API-36/PublicAPI.Unshipped.txt | 2 -- .../PublicAPI/API-37/PublicAPI.Unshipped.txt | 2 -- 6 files changed, 22 insertions(+), 11 deletions(-) diff --git a/src/Mono.Android/Android.Runtime/ResourceDesignerAttribute.cs b/src/Mono.Android/Android.Runtime/ResourceDesignerAttribute.cs index 1759fafdcd8..b6fde62e85d 100644 --- a/src/Mono.Android/Android.Runtime/ResourceDesignerAttribute.cs +++ b/src/Mono.Android/Android.Runtime/ResourceDesignerAttribute.cs @@ -1,5 +1,7 @@ using System; +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; +using System.Reflection; namespace Android.Runtime { @@ -8,6 +10,8 @@ public class ResourceDesignerAttribute : Attribute { const string UseResourceTypeConstructor = "Resource designer lookup by name requires dynamic code. Use ResourceDesignerAttribute(Type) instead."; + readonly Type? resourceType; + public ResourceDesignerAttribute ( [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicMethods)] Type resourceType) @@ -15,7 +19,7 @@ public ResourceDesignerAttribute ( if (resourceType == null) throw new ArgumentNullException (nameof (resourceType)); - ResourceType = resourceType; + this.resourceType = resourceType; FullName = resourceType.FullName ?? resourceType.Name; } @@ -28,7 +32,22 @@ public ResourceDesignerAttribute (string fullName) public string FullName { get; set; } [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicMethods)] - public Type? ResourceType { get; set; } + internal Type? GetResourceTypeFromAssembly (Assembly assembly) + { + if (resourceType != null) { + Debug.Assert (assembly == resourceType.Assembly); + return resourceType; + } + + const string legacyLookup = "The legacy string-based ResourceDesignerAttribute constructor requires dynamic code."; + + [UnconditionalSuppressMessage ("Trimming", "IL2026", Justification = legacyLookup)] + [UnconditionalSuppressMessage ("Trimming", "IL2073", Justification = legacyLookup)] + [return: DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicMethods)] + static Type? AssemblyGetType (Assembly assembly, string name) => assembly.GetType (name); + + return AssemblyGetType (assembly, FullName); + } public bool IsApplication { get; set; } } diff --git a/src/Mono.Android/Android.Runtime/ResourceIdManager.cs b/src/Mono.Android/Android.Runtime/ResourceIdManager.cs index 2886513f91c..05cdb3b843a 100644 --- a/src/Mono.Android/Android.Runtime/ResourceIdManager.cs +++ b/src/Mono.Android/Android.Runtime/ResourceIdManager.cs @@ -36,7 +36,7 @@ public static void UpdateIdValues () { foreach (var customAttribute in assembly.GetCustomAttributes (typeof (ResourceDesignerAttribute), true)) { if (customAttribute is ResourceDesignerAttribute resourceDesignerAttribute && resourceDesignerAttribute.IsApplication) { - return resourceDesignerAttribute.ResourceType; + return resourceDesignerAttribute.GetResourceTypeFromAssembly (assembly); } } return null; diff --git a/src/Mono.Android/PublicAPI/API-35/PublicAPI.Unshipped.txt b/src/Mono.Android/PublicAPI/API-35/PublicAPI.Unshipped.txt index ed6a6d1254c..d1d006954a2 100644 --- a/src/Mono.Android/PublicAPI/API-35/PublicAPI.Unshipped.txt +++ b/src/Mono.Android/PublicAPI/API-35/PublicAPI.Unshipped.txt @@ -2,5 +2,3 @@ Android.App.ApplicationAttribute.EnableOnBackInvokedCallback.get -> bool Android.App.ApplicationAttribute.EnableOnBackInvokedCallback.set -> void Android.Runtime.ResourceDesignerAttribute.ResourceDesignerAttribute(System.Type! resourceType) -> void -Android.Runtime.ResourceDesignerAttribute.ResourceType.get -> System.Type? -Android.Runtime.ResourceDesignerAttribute.ResourceType.set -> void diff --git a/src/Mono.Android/PublicAPI/API-36.1/PublicAPI.Unshipped.txt b/src/Mono.Android/PublicAPI/API-36.1/PublicAPI.Unshipped.txt index ed6a6d1254c..d1d006954a2 100644 --- a/src/Mono.Android/PublicAPI/API-36.1/PublicAPI.Unshipped.txt +++ b/src/Mono.Android/PublicAPI/API-36.1/PublicAPI.Unshipped.txt @@ -2,5 +2,3 @@ Android.App.ApplicationAttribute.EnableOnBackInvokedCallback.get -> bool Android.App.ApplicationAttribute.EnableOnBackInvokedCallback.set -> void Android.Runtime.ResourceDesignerAttribute.ResourceDesignerAttribute(System.Type! resourceType) -> void -Android.Runtime.ResourceDesignerAttribute.ResourceType.get -> System.Type? -Android.Runtime.ResourceDesignerAttribute.ResourceType.set -> void diff --git a/src/Mono.Android/PublicAPI/API-36/PublicAPI.Unshipped.txt b/src/Mono.Android/PublicAPI/API-36/PublicAPI.Unshipped.txt index cdf12e0bbdd..3e35dc64f8c 100644 --- a/src/Mono.Android/PublicAPI/API-36/PublicAPI.Unshipped.txt +++ b/src/Mono.Android/PublicAPI/API-36/PublicAPI.Unshipped.txt @@ -1973,8 +1973,6 @@ Android.Ranging.Wifi.Rtt.RttRangingParams.RangingUpdateRate.get -> Android.Rangi Android.Ranging.Wifi.Rtt.RttRangingParams.ServiceName.get -> string! Android.Ranging.Wifi.Rtt.RttRangingParams.WriteToParcel(Android.OS.Parcel! dest, Android.OS.ParcelableWriteFlags flags) -> void Android.Runtime.ResourceDesignerAttribute.ResourceDesignerAttribute(System.Type! resourceType) -> void -Android.Runtime.ResourceDesignerAttribute.ResourceType.get -> System.Type? -Android.Runtime.ResourceDesignerAttribute.ResourceType.set -> void Android.Security.AdvancedProtection.AdvancedProtectionManager Android.Security.AdvancedProtection.AdvancedProtectionManager.ICallback Android.Security.AdvancedProtection.AdvancedProtectionManager.ICallback.OnAdvancedProtectionChanged(bool enabled) -> void diff --git a/src/Mono.Android/PublicAPI/API-37/PublicAPI.Unshipped.txt b/src/Mono.Android/PublicAPI/API-37/PublicAPI.Unshipped.txt index ed6a6d1254c..d1d006954a2 100644 --- a/src/Mono.Android/PublicAPI/API-37/PublicAPI.Unshipped.txt +++ b/src/Mono.Android/PublicAPI/API-37/PublicAPI.Unshipped.txt @@ -2,5 +2,3 @@ Android.App.ApplicationAttribute.EnableOnBackInvokedCallback.get -> bool Android.App.ApplicationAttribute.EnableOnBackInvokedCallback.set -> void Android.Runtime.ResourceDesignerAttribute.ResourceDesignerAttribute(System.Type! resourceType) -> void -Android.Runtime.ResourceDesignerAttribute.ResourceType.get -> System.Type? -Android.Runtime.ResourceDesignerAttribute.ResourceType.set -> void From b2fed7e52b23d1a789837de19ac21d18977f1241 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 21 May 2026 15:21:42 +0200 Subject: [PATCH 04/25] Return null for resource designer assembly mismatch When ResourceDesignerAttribute is constructed with a Type, ignore assemblies other than the resource type's declaring assembly instead of asserting. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Mono.Android/Android.Runtime/ResourceDesignerAttribute.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Mono.Android/Android.Runtime/ResourceDesignerAttribute.cs b/src/Mono.Android/Android.Runtime/ResourceDesignerAttribute.cs index b6fde62e85d..aeb7f190ca0 100644 --- a/src/Mono.Android/Android.Runtime/ResourceDesignerAttribute.cs +++ b/src/Mono.Android/Android.Runtime/ResourceDesignerAttribute.cs @@ -1,5 +1,4 @@ using System; -using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Reflection; @@ -35,8 +34,7 @@ public ResourceDesignerAttribute (string fullName) internal Type? GetResourceTypeFromAssembly (Assembly assembly) { if (resourceType != null) { - Debug.Assert (assembly == resourceType.Assembly); - return resourceType; + return assembly == resourceType.Assembly ? resourceType : null; } const string legacyLookup = "The legacy string-based ResourceDesignerAttribute constructor requires dynamic code."; From d026b03fe2a3f90cdadcace5319a11f3fcd5e8cd Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 21 May 2026 15:25:13 +0200 Subject: [PATCH 05/25] Use RUC for legacy resource designer attribute The legacy string-based ResourceDesignerAttribute path reaches Assembly.GetType(string), which is annotated RequiresUnreferencedCode. Mark the compatibility constructor accordingly while keeping the Type constructor as the safe generated path. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Android.Runtime/ResourceDesignerAttribute.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Mono.Android/Android.Runtime/ResourceDesignerAttribute.cs b/src/Mono.Android/Android.Runtime/ResourceDesignerAttribute.cs index aeb7f190ca0..17f6978b44f 100644 --- a/src/Mono.Android/Android.Runtime/ResourceDesignerAttribute.cs +++ b/src/Mono.Android/Android.Runtime/ResourceDesignerAttribute.cs @@ -7,7 +7,7 @@ namespace Android.Runtime [AttributeUsage (AttributeTargets.Assembly)] public class ResourceDesignerAttribute : Attribute { - const string UseResourceTypeConstructor = "Resource designer lookup by name requires dynamic code. Use ResourceDesignerAttribute(Type) instead."; + const string UseResourceTypeConstructor = "Resource designer lookup by name requires unreferenced code. Use ResourceDesignerAttribute(Type) instead."; readonly Type? resourceType; @@ -22,7 +22,7 @@ public ResourceDesignerAttribute ( FullName = resourceType.FullName ?? resourceType.Name; } - [RequiresDynamicCode (UseResourceTypeConstructor)] + [RequiresUnreferencedCode (UseResourceTypeConstructor)] public ResourceDesignerAttribute (string fullName) { FullName = fullName; @@ -37,7 +37,7 @@ public ResourceDesignerAttribute (string fullName) return assembly == resourceType.Assembly ? resourceType : null; } - const string legacyLookup = "The legacy string-based ResourceDesignerAttribute constructor requires dynamic code."; + const string legacyLookup = "The legacy string-based ResourceDesignerAttribute constructor requires unreferenced code."; [UnconditionalSuppressMessage ("Trimming", "IL2026", Justification = legacyLookup)] [UnconditionalSuppressMessage ("Trimming", "IL2073", Justification = legacyLookup)] From 0d4d5b95384c9458644fcb23556629911321c59d Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 21 May 2026 15:27:13 +0200 Subject: [PATCH 06/25] Continue scanning resource designer attributes Only return from ResourceIdManager when ResourceDesignerAttribute resolves a non-null resource type, allowing mismatched Type-based attributes to be skipped. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Mono.Android/Android.Runtime/ResourceIdManager.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Mono.Android/Android.Runtime/ResourceIdManager.cs b/src/Mono.Android/Android.Runtime/ResourceIdManager.cs index 05cdb3b843a..0275fbc6195 100644 --- a/src/Mono.Android/Android.Runtime/ResourceIdManager.cs +++ b/src/Mono.Android/Android.Runtime/ResourceIdManager.cs @@ -36,7 +36,9 @@ public static void UpdateIdValues () { foreach (var customAttribute in assembly.GetCustomAttributes (typeof (ResourceDesignerAttribute), true)) { if (customAttribute is ResourceDesignerAttribute resourceDesignerAttribute && resourceDesignerAttribute.IsApplication) { - return resourceDesignerAttribute.GetResourceTypeFromAssembly (assembly); + var type = resourceDesignerAttribute.GetResourceTypeFromAssembly (assembly); + if (type != null) + return type; } } return null; From a12930a456071820720f1906533723fe26e9b0fd Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 21 May 2026 15:30:17 +0200 Subject: [PATCH 07/25] Handle Type resource designer import arguments ResourceDesignerAttribute may now be generated with typeof(Resource). When importing resource designer attributes from metadata, use Type.FullName for Type fixed arguments while preserving legacy string arguments. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Utilities/ResourceDesignerImportGenerator.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/ResourceDesignerImportGenerator.cs b/src/Xamarin.Android.Build.Tasks/Utilities/ResourceDesignerImportGenerator.cs index d86fb3fadf4..892826501f5 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/ResourceDesignerImportGenerator.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/ResourceDesignerImportGenerator.cs @@ -99,7 +99,11 @@ public void CreateImportMethods (IEnumerable libraries) return null; } } - return values.FixedArguments.First ().Value as string; + return values.FixedArguments.First ().Value switch { + Type type => type.FullName ?? type.Name, + string name => name, + _ => null, + }; } } return null; From 65be7bc498cd5dd4b0c972c2e205d0af7341e291 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 21 May 2026 15:33:41 +0200 Subject: [PATCH 08/25] Keep dummy attribute provider unchanged Decode ResourceDesignerAttribute type arguments with a local provider so DummyCustomAttributeProvider keeps its existing behavior while resource designer imports still handle typeof(Resource). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Utilities/DummyCustomAttributeProvider.cs | 2 +- .../ResourceDesignerImportGenerator.cs | 29 +++++++++++++++---- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/DummyCustomAttributeProvider.cs b/src/Xamarin.Android.Build.Tasks/Utilities/DummyCustomAttributeProvider.cs index 7489de33838..a4d5869c42c 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/DummyCustomAttributeProvider.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/DummyCustomAttributeProvider.cs @@ -27,7 +27,7 @@ public class DummyCustomAttributeProvider : ICustomAttributeTypeProvider null; - public object? GetTypeFromSerializedName (string name) => name; + public object? GetTypeFromSerializedName (string name) => null; public PrimitiveTypeCode GetUnderlyingEnumType (object? type) => default (PrimitiveTypeCode); diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/ResourceDesignerImportGenerator.cs b/src/Xamarin.Android.Build.Tasks/Utilities/ResourceDesignerImportGenerator.cs index 892826501f5..877ee92645d 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/ResourceDesignerImportGenerator.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/ResourceDesignerImportGenerator.cs @@ -92,23 +92,40 @@ public void CreateImportMethods (IEnumerable libraries) var attribute = reader.GetCustomAttribute (handle); var fullName = reader.GetCustomAttributeFullName (attribute, Log); if (fullName == "Android.Runtime.ResourceDesignerAttribute") { - var values = attribute.GetCustomAttributeArguments (); + var values = attribute.DecodeValue (ResourceDesignerAttributeTypeProvider.Instance); foreach (var arg in values.NamedArguments) { // application resource IDs are constants, cannot merge. if (arg.Name == "IsApplication" && arg.Value is bool isApplication && isApplication) { return null; } } - return values.FixedArguments.First ().Value switch { - Type type => type.FullName ?? type.Name, - string name => name, - _ => null, - }; + return values.FixedArguments.First ().Value as string; } } return null; } + sealed class ResourceDesignerAttributeTypeProvider : ICustomAttributeTypeProvider + { + public static readonly ResourceDesignerAttributeTypeProvider Instance = new ResourceDesignerAttributeTypeProvider (); + + public object? GetPrimitiveType (PrimitiveTypeCode typeCode) => null; + + public object? GetSystemType () => null; + + public object? GetSZArrayType (object? elementType) => null; + + public object? GetTypeFromDefinition (MetadataReader reader, TypeDefinitionHandle handle, byte rawTypeKind) => null; + + public object? GetTypeFromReference (MetadataReader reader, TypeReferenceHandle handle, byte rawTypeKind) => null; + + public object? GetTypeFromSerializedName (string name) => name; + + public PrimitiveTypeCode GetUnderlyingEnumType (object? type) => default (PrimitiveTypeCode); + + public bool IsSystemType (object? type) => false; + } + void CreateImportFor (string declaringTypeFullName, TypeDefinition type, CodeMemberMethod method, MetadataReader reader, bool hasAlias) { var typeName = reader.GetString (type.Name); From 75e8ee9be928b2a82efeb531fb1c249d606ccf45 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 21 May 2026 15:41:26 +0200 Subject: [PATCH 09/25] Use provider strategy for cleaner separation of concerns --- .../ResourceDesignerAttribute.cs | 51 ++++++++++--------- 1 file changed, 28 insertions(+), 23 deletions(-) diff --git a/src/Mono.Android/Android.Runtime/ResourceDesignerAttribute.cs b/src/Mono.Android/Android.Runtime/ResourceDesignerAttribute.cs index 17f6978b44f..255ae596471 100644 --- a/src/Mono.Android/Android.Runtime/ResourceDesignerAttribute.cs +++ b/src/Mono.Android/Android.Runtime/ResourceDesignerAttribute.cs @@ -9,44 +9,49 @@ public class ResourceDesignerAttribute : Attribute { const string UseResourceTypeConstructor = "Resource designer lookup by name requires unreferenced code. Use ResourceDesignerAttribute(Type) instead."; - readonly Type? resourceType; + readonly IResourceTypeProvider provider; public ResourceDesignerAttribute ( - [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicMethods)] - Type resourceType) + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicMethods)] + Type resourceType) { - if (resourceType == null) - throw new ArgumentNullException (nameof (resourceType)); - - this.resourceType = resourceType; - FullName = resourceType.FullName ?? resourceType.Name; + provider = new TypeResourceTypeProvider (resourceType); } [RequiresUnreferencedCode (UseResourceTypeConstructor)] public ResourceDesignerAttribute (string fullName) { - FullName = fullName; + provider = new StringResourceTypeProvider (fullName); } - public string FullName { get; set; } - - [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicMethods)] - internal Type? GetResourceTypeFromAssembly (Assembly assembly) + public string FullName { - if (resourceType != null) { - return assembly == resourceType.Assembly ? resourceType : null; - } + get => provider.FullName; + set => throw new NotSupportedException ("Resource designer lookup by name does not support setting the full name."); + } - const string legacyLookup = "The legacy string-based ResourceDesignerAttribute constructor requires unreferenced code."; + [return: DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicMethods)] + internal Type? GetResourceTypeFromAssembly (Assembly assembly) => provider.GetResourceTypeFromAssembly (assembly); - [UnconditionalSuppressMessage ("Trimming", "IL2026", Justification = legacyLookup)] - [UnconditionalSuppressMessage ("Trimming", "IL2073", Justification = legacyLookup)] - [return: DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicMethods)] - static Type? AssemblyGetType (Assembly assembly, string name) => assembly.GetType (name); + public bool IsApplication { get; set; } - return AssemblyGetType (assembly, FullName); + private interface IResourceTypeProvider + { + Type? GetResourceTypeFromAssembly (Assembly assembly); + string FullName { get; } } - public bool IsApplication { get; set; } + private sealed class TypeResourceTypeProvider([DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicMethods)] Type resourceType) : IResourceTypeProvider + { + public Type? GetResourceTypeFromAssembly (Assembly assembly) => assembly == resourceType.Assembly ? resourceType : null; + public string FullName => resourceType.FullName ?? resourceType.Name; + } + + [RequiresUnreferencedCode (UseResourceTypeConstructor)] + private sealed class StringResourceTypeProvider(string fullName) : IResourceTypeProvider + { + public Type? GetResourceTypeFromAssembly (Assembly assembly) => assembly.GetType (fullName); + public string FullName => fullName; + } } } From c0c8fe173c4739a20556c7eded1513cc3cfe9ccb Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 21 May 2026 15:47:10 +0200 Subject: [PATCH 10/25] Decode resource designer attribute argument directly Remove the local ResourceDesignerAttributeTypeProvider and read the serialized fixed attribute argument directly so both string and typeof(Resource) constructors are handled without changing the shared dummy provider. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../ResourceDesignerImportGenerator.cs | 27 +++++-------------- 1 file changed, 7 insertions(+), 20 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/ResourceDesignerImportGenerator.cs b/src/Xamarin.Android.Build.Tasks/Utilities/ResourceDesignerImportGenerator.cs index 877ee92645d..8e0f4756467 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/ResourceDesignerImportGenerator.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/ResourceDesignerImportGenerator.cs @@ -92,38 +92,25 @@ public void CreateImportMethods (IEnumerable libraries) var attribute = reader.GetCustomAttribute (handle); var fullName = reader.GetCustomAttributeFullName (attribute, Log); if (fullName == "Android.Runtime.ResourceDesignerAttribute") { - var values = attribute.DecodeValue (ResourceDesignerAttributeTypeProvider.Instance); + var values = attribute.GetCustomAttributeArguments (); foreach (var arg in values.NamedArguments) { // application resource IDs are constants, cannot merge. if (arg.Name == "IsApplication" && arg.Value is bool isApplication && isApplication) { return null; } } - return values.FixedArguments.First ().Value as string; + return GetResourceDesignerTypeName (reader, attribute); } } return null; } - sealed class ResourceDesignerAttributeTypeProvider : ICustomAttributeTypeProvider + static string? GetResourceDesignerTypeName (MetadataReader reader, CustomAttribute attribute) { - public static readonly ResourceDesignerAttributeTypeProvider Instance = new ResourceDesignerAttributeTypeProvider (); - - public object? GetPrimitiveType (PrimitiveTypeCode typeCode) => null; - - public object? GetSystemType () => null; - - public object? GetSZArrayType (object? elementType) => null; - - public object? GetTypeFromDefinition (MetadataReader reader, TypeDefinitionHandle handle, byte rawTypeKind) => null; - - public object? GetTypeFromReference (MetadataReader reader, TypeReferenceHandle handle, byte rawTypeKind) => null; - - public object? GetTypeFromSerializedName (string name) => name; - - public PrimitiveTypeCode GetUnderlyingEnumType (object? type) => default (PrimitiveTypeCode); - - public bool IsSystemType (object? type) => false; + var value = reader.GetBlobReader (attribute.Value); + if (value.ReadUInt16 () != 1) + return null; + return value.ReadSerializedString (); } void CreateImportFor (string declaringTypeFullName, TypeDefinition type, CodeMemberMethod method, MetadataReader reader, bool hasAlias) From 8ed1be615b7fa84c4ca388214c1b244400f56197 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 21 May 2026 15:52:52 +0200 Subject: [PATCH 11/25] Explain resource designer attribute decoding Document why reading the first fixed argument as a serialized string works for both string and System.Type ResourceDesignerAttribute constructors. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Utilities/ResourceDesignerImportGenerator.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/ResourceDesignerImportGenerator.cs b/src/Xamarin.Android.Build.Tasks/Utilities/ResourceDesignerImportGenerator.cs index 8e0f4756467..790d57b02ca 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/ResourceDesignerImportGenerator.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/ResourceDesignerImportGenerator.cs @@ -107,10 +107,13 @@ public void CreateImportMethods (IEnumerable libraries) static string? GetResourceDesignerTypeName (MetadataReader reader, CustomAttribute attribute) { - var value = reader.GetBlobReader (attribute.Value); - if (value.ReadUInt16 () != 1) + var attributeValue = reader.GetBlobReader (attribute.Value); + if (attributeValue.ReadUInt16 () != 1) return null; - return value.ReadSerializedString (); + + // ResourceDesignerAttribute has one fixed argument. In a custom attribute blob, + // both string arguments and System.Type arguments are encoded as a SerString. + return attributeValue.ReadSerializedString (); } void CreateImportFor (string declaringTypeFullName, TypeDefinition type, CodeMemberMethod method, MetadataReader reader, bool hasAlias) From cb89b78bf31606b6411e98b89622b2187c8349a6 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 21 May 2026 16:10:48 +0200 Subject: [PATCH 12/25] Test typeof resource designer imports Add a build-task test proving ResourceDesignerImportGenerator imports library resources when ResourceDesignerAttribute is emitted with typeof(Resource), and remove the experimental provider strategy from ResourceDesignerAttribute. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../ResourceDesignerAttribute.cs | 49 +++++++++---------- .../Tasks/ManagedResourceParserTests.cs | 24 +++++++++ 2 files changed, 46 insertions(+), 27 deletions(-) diff --git a/src/Mono.Android/Android.Runtime/ResourceDesignerAttribute.cs b/src/Mono.Android/Android.Runtime/ResourceDesignerAttribute.cs index 255ae596471..660ddaccf03 100644 --- a/src/Mono.Android/Android.Runtime/ResourceDesignerAttribute.cs +++ b/src/Mono.Android/Android.Runtime/ResourceDesignerAttribute.cs @@ -9,49 +9,44 @@ public class ResourceDesignerAttribute : Attribute { const string UseResourceTypeConstructor = "Resource designer lookup by name requires unreferenced code. Use ResourceDesignerAttribute(Type) instead."; - readonly IResourceTypeProvider provider; + readonly Type? resourceType; public ResourceDesignerAttribute ( - [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicMethods)] - Type resourceType) + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicMethods)] + Type resourceType) { - provider = new TypeResourceTypeProvider (resourceType); + if (resourceType == null) + throw new ArgumentNullException (nameof (resourceType)); + + this.resourceType = resourceType; + FullName = resourceType.FullName ?? resourceType.Name; } [RequiresUnreferencedCode (UseResourceTypeConstructor)] public ResourceDesignerAttribute (string fullName) { - provider = new StringResourceTypeProvider (fullName); + FullName = fullName; } - public string FullName - { - get => provider.FullName; - set => throw new NotSupportedException ("Resource designer lookup by name does not support setting the full name."); - } + public string FullName { get; set; } [return: DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicMethods)] - internal Type? GetResourceTypeFromAssembly (Assembly assembly) => provider.GetResourceTypeFromAssembly (assembly); + internal Type? GetResourceTypeFromAssembly (Assembly assembly) + { + if (resourceType != null) { + return assembly == resourceType.Assembly ? resourceType : null; + } - public bool IsApplication { get; set; } + const string legacyLookup = "The legacy string-based ResourceDesignerAttribute constructor requires unreferenced code."; - private interface IResourceTypeProvider - { - Type? GetResourceTypeFromAssembly (Assembly assembly); - string FullName { get; } - } + [UnconditionalSuppressMessage ("Trimming", "IL2026", Justification = legacyLookup)] + [UnconditionalSuppressMessage ("Trimming", "IL2073", Justification = legacyLookup)] + [return: DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicMethods)] + static Type? AssemblyGetType (Assembly assembly, string name) => assembly.GetType (name); - private sealed class TypeResourceTypeProvider([DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicMethods)] Type resourceType) : IResourceTypeProvider - { - public Type? GetResourceTypeFromAssembly (Assembly assembly) => assembly == resourceType.Assembly ? resourceType : null; - public string FullName => resourceType.FullName ?? resourceType.Name; + return AssemblyGetType (assembly, FullName); } - [RequiresUnreferencedCode (UseResourceTypeConstructor)] - private sealed class StringResourceTypeProvider(string fullName) : IResourceTypeProvider - { - public Type? GetResourceTypeFromAssembly (Assembly assembly) => assembly.GetType (fullName); - public string FullName => fullName; - } + public bool IsApplication { get; set; } } } diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/ManagedResourceParserTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/ManagedResourceParserTests.cs index 21edf2f634d..1b1811da6ed 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/ManagedResourceParserTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/ManagedResourceParserTests.cs @@ -460,6 +460,30 @@ public void GenerateDesignerFileFromRtxt ([Values] bool withLibraryReference, [V Directory.Delete (Path.Combine (Root, path), recursive: true); } + [Test] + public void ResourceDesignerImportGeneratorHandlesTypeResourceDesignerAttribute () + { + var path = Path.Combine ("temp", TestName + " Some Space"); + CreateResourceDirectory (path); + var mapTask = CreateCaseMapTask (path); + Assert.IsTrue (mapTask.Execute (), "Map Task should have executed successfully."); + + var libraryPath = Path.Combine (path, "Library"); + BuildLibraryWithResources (libraryPath, AndroidRuntime.MonoVM); + + var task = CreateTask (path); + task.RTxtFile = Path.Combine (Root, path, "R.txt"); + File.WriteAllText (task.RTxtFile, Rtxt); + task.References = new TaskItem [] { + new TaskItem (Path.Combine (Root, libraryPath, "bin", "Debug", "Library.dll")) + }; + + Assert.IsTrue (task.Execute (), "Task should have executed successfully."); + var designer = File.ReadAllText (task.NetResgenOutputFile); + StringAssert.Contains ("global::Library.Resource.Animator.slide_in_bottom = global::Foo.Foo.Resource.Animator.slide_in_bottom;", designer); + Directory.Delete (Path.Combine (Root, path), recursive: true); + } + [Test] public void GenerateDesignerFileFromEmptyRtxt () { From 6a6b180bdb0a8012b1be8de1c21fb94b653a84c9 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 21 May 2026 16:37:01 +0200 Subject: [PATCH 13/25] fixup! Test typeof resource designer imports --- .../ResourceDesignerAttribute.cs | 55 +++++++++++-------- .../Tasks/ManagedResourceParserTests.cs | 4 ++ 2 files changed, 37 insertions(+), 22 deletions(-) diff --git a/src/Mono.Android/Android.Runtime/ResourceDesignerAttribute.cs b/src/Mono.Android/Android.Runtime/ResourceDesignerAttribute.cs index 660ddaccf03..ee60e0fe535 100644 --- a/src/Mono.Android/Android.Runtime/ResourceDesignerAttribute.cs +++ b/src/Mono.Android/Android.Runtime/ResourceDesignerAttribute.cs @@ -9,44 +9,55 @@ public class ResourceDesignerAttribute : Attribute { const string UseResourceTypeConstructor = "Resource designer lookup by name requires unreferenced code. Use ResourceDesignerAttribute(Type) instead."; - readonly Type? resourceType; + readonly IResourceTypeProvider provider; public ResourceDesignerAttribute ( - [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicMethods)] - Type resourceType) + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicMethods)] + Type resourceType) { - if (resourceType == null) - throw new ArgumentNullException (nameof (resourceType)); - - this.resourceType = resourceType; - FullName = resourceType.FullName ?? resourceType.Name; + provider = new TypeResourceTypeProvider (resourceType); } [RequiresUnreferencedCode (UseResourceTypeConstructor)] - public ResourceDesignerAttribute (string fullName) + public ResourceDesignerAttribute ( + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicMethods)] + string fullName) { - FullName = fullName; + provider = new StringResourceTypeProvider (fullName); } - public string FullName { get; set; } + public string FullName + { + get => provider.FullName; + set => throw new NotSupportedException ("Resource designer lookup by name does not support setting the full name."); + } [return: DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicMethods)] - internal Type? GetResourceTypeFromAssembly (Assembly assembly) - { - if (resourceType != null) { - return assembly == resourceType.Assembly ? resourceType : null; - } + internal Type? GetResourceTypeFromAssembly (Assembly assembly) => provider.GetResourceTypeFromAssembly (assembly); - const string legacyLookup = "The legacy string-based ResourceDesignerAttribute constructor requires unreferenced code."; + public bool IsApplication { get; set; } - [UnconditionalSuppressMessage ("Trimming", "IL2026", Justification = legacyLookup)] - [UnconditionalSuppressMessage ("Trimming", "IL2073", Justification = legacyLookup)] + private interface IResourceTypeProvider + { [return: DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicMethods)] - static Type? AssemblyGetType (Assembly assembly, string name) => assembly.GetType (name); + Type? GetResourceTypeFromAssembly (Assembly assembly); + string FullName { get; } + } - return AssemblyGetType (assembly, FullName); + private sealed class TypeResourceTypeProvider([DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicMethods)] Type resourceType) : IResourceTypeProvider + { + [return: DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicMethods)] + public Type? GetResourceTypeFromAssembly (Assembly assembly) => assembly == resourceType.Assembly ? resourceType : null; + public string FullName => resourceType.FullName ?? resourceType.Name; } - public bool IsApplication { get; set; } + [RequiresUnreferencedCode (UseResourceTypeConstructor)] + private sealed class StringResourceTypeProvider(string fullName) : IResourceTypeProvider + { + [return: DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicMethods)] + public Type? GetResourceTypeFromAssembly (Assembly assembly) => assembly.GetType (fullName); + + public string FullName => fullName; + } } } diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/ManagedResourceParserTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/ManagedResourceParserTests.cs index 1b1811da6ed..95d016ae7b2 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/ManagedResourceParserTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/ManagedResourceParserTests.cs @@ -469,6 +469,8 @@ public void ResourceDesignerImportGeneratorHandlesTypeResourceDesignerAttribute Assert.IsTrue (mapTask.Execute (), "Map Task should have executed successfully."); var libraryPath = Path.Combine (path, "Library"); + // BuildLibraryWithResources() generates a library Resource.designer.cs with: + // [assembly: ResourceDesignerAttribute(typeof(Library.Resource), IsApplication=false)] BuildLibraryWithResources (libraryPath, AndroidRuntime.MonoVM); var task = CreateTask (path); @@ -480,6 +482,8 @@ public void ResourceDesignerImportGeneratorHandlesTypeResourceDesignerAttribute Assert.IsTrue (task.Execute (), "Task should have executed successfully."); var designer = File.ReadAllText (task.NetResgenOutputFile); + // The import generator can only emit this assignment if it decoded the + // typeof(Library.Resource) attribute argument back to "Library.Resource". StringAssert.Contains ("global::Library.Resource.Animator.slide_in_bottom = global::Foo.Foo.Resource.Animator.slide_in_bottom;", designer); Directory.Delete (Path.Combine (Root, path), recursive: true); } From d3d74d1c843aae08524ed4a6de5cbb60a90b129f Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 21 May 2026 17:15:54 +0200 Subject: [PATCH 14/25] Decode resource designer named arguments directly Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../ResourceDesignerImportGenerator.cs | 38 ++++++++++++++++--- 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/ResourceDesignerImportGenerator.cs b/src/Xamarin.Android.Build.Tasks/Utilities/ResourceDesignerImportGenerator.cs index 790d57b02ca..40e8d2a5235 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/ResourceDesignerImportGenerator.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/ResourceDesignerImportGenerator.cs @@ -92,12 +92,9 @@ public void CreateImportMethods (IEnumerable libraries) var attribute = reader.GetCustomAttribute (handle); var fullName = reader.GetCustomAttributeFullName (attribute, Log); if (fullName == "Android.Runtime.ResourceDesignerAttribute") { - var values = attribute.GetCustomAttributeArguments (); - foreach (var arg in values.NamedArguments) { - // application resource IDs are constants, cannot merge. - if (arg.Name == "IsApplication" && arg.Value is bool isApplication && isApplication) { - return null; - } + // application resource IDs are constants, cannot merge. + if (IsApplicationResourceDesigner (reader, attribute)) { + return null; } return GetResourceDesignerTypeName (reader, attribute); } @@ -116,6 +113,35 @@ public void CreateImportMethods (IEnumerable libraries) return attributeValue.ReadSerializedString (); } + static bool IsApplicationResourceDesigner (MetadataReader reader, CustomAttribute attribute) + { + var attributeValue = reader.GetBlobReader (attribute.Value); + if (attributeValue.ReadUInt16 () != 1) + return false; + + attributeValue.ReadSerializedString (); + var namedArgumentCount = attributeValue.ReadUInt16 (); + for (int i = 0; i < namedArgumentCount; i++) { + const byte Field = 0x53; + const byte Property = 0x54; + const byte Boolean = 0x02; + + var memberKind = attributeValue.ReadByte (); + if (memberKind != Field && memberKind != Property) + return false; + + var memberType = attributeValue.ReadByte (); + if (memberType != Boolean) + return false; + + var memberName = attributeValue.ReadSerializedString (); + var memberValue = attributeValue.ReadByte () != 0; + if (memberName == "IsApplication") + return memberValue; + } + return false; + } + void CreateImportFor (string declaringTypeFullName, TypeDefinition type, CodeMemberMethod method, MetadataReader reader, bool hasAlias) { var typeName = reader.GetString (type.Name); From d3b944d12ef9705486076c3aacb37f8e404fc7f9 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 21 May 2026 23:12:41 +0200 Subject: [PATCH 15/25] Address resource designer review feedback Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Android.Runtime/ResourceDesignerAttribute.cs | 5 +++-- .../Utilities/ResourceDesignerImportGenerator.cs | 12 ++++++------ 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/Mono.Android/Android.Runtime/ResourceDesignerAttribute.cs b/src/Mono.Android/Android.Runtime/ResourceDesignerAttribute.cs index ee60e0fe535..89cb5a56978 100644 --- a/src/Mono.Android/Android.Runtime/ResourceDesignerAttribute.cs +++ b/src/Mono.Android/Android.Runtime/ResourceDesignerAttribute.cs @@ -9,7 +9,7 @@ public class ResourceDesignerAttribute : Attribute { const string UseResourceTypeConstructor = "Resource designer lookup by name requires unreferenced code. Use ResourceDesignerAttribute(Type) instead."; - readonly IResourceTypeProvider provider; + IResourceTypeProvider provider; public ResourceDesignerAttribute ( [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicMethods)] @@ -29,7 +29,8 @@ public ResourceDesignerAttribute ( public string FullName { get => provider.FullName; - set => throw new NotSupportedException ("Resource designer lookup by name does not support setting the full name."); + [RequiresUnreferencedCode (UseResourceTypeConstructor)] + set => provider = new StringResourceTypeProvider (value); } [return: DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicMethods)] diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/ResourceDesignerImportGenerator.cs b/src/Xamarin.Android.Build.Tasks/Utilities/ResourceDesignerImportGenerator.cs index 40e8d2a5235..28d54034a2b 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/ResourceDesignerImportGenerator.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/ResourceDesignerImportGenerator.cs @@ -93,29 +93,29 @@ public void CreateImportMethods (IEnumerable libraries) var fullName = reader.GetCustomAttributeFullName (attribute, Log); if (fullName == "Android.Runtime.ResourceDesignerAttribute") { // application resource IDs are constants, cannot merge. - if (IsApplicationResourceDesigner (reader, attribute)) { + if (IsApplicationResourceDesigner (reader.GetBlobReader (attribute.Value))) { return null; } - return GetResourceDesignerTypeName (reader, attribute); + return GetResourceDesignerTypeName (reader.GetBlobReader (attribute.Value)); } } return null; } - static string? GetResourceDesignerTypeName (MetadataReader reader, CustomAttribute attribute) + static string? GetResourceDesignerTypeName (BlobReader attributeValue) { - var attributeValue = reader.GetBlobReader (attribute.Value); if (attributeValue.ReadUInt16 () != 1) return null; // ResourceDesignerAttribute has one fixed argument. In a custom attribute blob, // both string arguments and System.Type arguments are encoded as a SerString. + // For System.Type in the same assembly, that string is namespace-qualified + // instead of assembly-qualified, matching the resource designer type name. return attributeValue.ReadSerializedString (); } - static bool IsApplicationResourceDesigner (MetadataReader reader, CustomAttribute attribute) + static bool IsApplicationResourceDesigner (BlobReader attributeValue) { - var attributeValue = reader.GetBlobReader (attribute.Value); if (attributeValue.ReadUInt16 () != 1) return false; From 99877b06b5382b0136b1d58884b9a6cac2c56ccf Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 22 May 2026 11:27:04 +0200 Subject: [PATCH 16/25] Simplify resource designer attribute decoding Decode System.Type custom attribute arguments through DummyCustomAttributeProvider so ResourceDesignerImportGenerator can use the shared GetCustomAttributeArguments() helper for both typeof(Resource) and string ResourceDesignerAttribute constructors. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Utilities/DummyCustomAttributeProvider.cs | 21 ++++++-- .../ResourceDesignerImportGenerator.cs | 52 +++---------------- 2 files changed, 23 insertions(+), 50 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/DummyCustomAttributeProvider.cs b/src/Xamarin.Android.Build.Tasks/Utilities/DummyCustomAttributeProvider.cs index a4d5869c42c..08532fe7a24 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/DummyCustomAttributeProvider.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/DummyCustomAttributeProvider.cs @@ -16,21 +16,32 @@ namespace Xamarin.Android.Tasks public class DummyCustomAttributeProvider : ICustomAttributeTypeProvider { public static readonly DummyCustomAttributeProvider Instance = new DummyCustomAttributeProvider (); + static readonly object systemType = new object (); public object? GetPrimitiveType (PrimitiveTypeCode typeCode) => null; - public object? GetSystemType () => null; + public object? GetSystemType () => systemType; public object? GetSZArrayType (object? elementType) => null; - public object? GetTypeFromDefinition (MetadataReader reader, TypeDefinitionHandle handle, byte rawTypeKind) => null; + public object? GetTypeFromDefinition (MetadataReader reader, TypeDefinitionHandle handle, byte rawTypeKind) + { + var type = reader.GetTypeDefinition (handle); + return IsSystemType (reader.GetString (type.Namespace), reader.GetString (type.Name)) ? systemType : null; + } - public object? GetTypeFromReference (MetadataReader reader, TypeReferenceHandle handle, byte rawTypeKind) => null; + public object? GetTypeFromReference (MetadataReader reader, TypeReferenceHandle handle, byte rawTypeKind) + { + var type = reader.GetTypeReference (handle); + return IsSystemType (reader.GetString (type.Namespace), reader.GetString (type.Name)) ? systemType : null; + } - public object? GetTypeFromSerializedName (string name) => null; + public object? GetTypeFromSerializedName (string name) => name; public PrimitiveTypeCode GetUnderlyingEnumType (object? type) => default (PrimitiveTypeCode); - public bool IsSystemType (object? type) => false; + public bool IsSystemType (object? type) => type == systemType; + + static bool IsSystemType (string ns, string name) => ns == "System" && name == "Type"; } } diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/ResourceDesignerImportGenerator.cs b/src/Xamarin.Android.Build.Tasks/Utilities/ResourceDesignerImportGenerator.cs index 28d54034a2b..2a85c78ce2a 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/ResourceDesignerImportGenerator.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/ResourceDesignerImportGenerator.cs @@ -2,7 +2,6 @@ using System.CodeDom; using System.Collections.Generic; using System.IO; -using System.Linq; using System.Reflection.Metadata; using System.Reflection.PortableExecutable; using Microsoft.Build.Framework; @@ -92,56 +91,19 @@ public void CreateImportMethods (IEnumerable libraries) var attribute = reader.GetCustomAttribute (handle); var fullName = reader.GetCustomAttributeFullName (attribute, Log); if (fullName == "Android.Runtime.ResourceDesignerAttribute") { - // application resource IDs are constants, cannot merge. - if (IsApplicationResourceDesigner (reader.GetBlobReader (attribute.Value))) { - return null; + var values = attribute.GetCustomAttributeArguments (); + foreach (var arg in values.NamedArguments) { + // application resource IDs are constants, cannot merge. + if (arg.Name == "IsApplication" && arg.Value is bool isApplication && isApplication) { + return null; + } } - return GetResourceDesignerTypeName (reader.GetBlobReader (attribute.Value)); + return values.FixedArguments.Length > 0 ? values.FixedArguments [0].Value as string : null; } } return null; } - static string? GetResourceDesignerTypeName (BlobReader attributeValue) - { - if (attributeValue.ReadUInt16 () != 1) - return null; - - // ResourceDesignerAttribute has one fixed argument. In a custom attribute blob, - // both string arguments and System.Type arguments are encoded as a SerString. - // For System.Type in the same assembly, that string is namespace-qualified - // instead of assembly-qualified, matching the resource designer type name. - return attributeValue.ReadSerializedString (); - } - - static bool IsApplicationResourceDesigner (BlobReader attributeValue) - { - if (attributeValue.ReadUInt16 () != 1) - return false; - - attributeValue.ReadSerializedString (); - var namedArgumentCount = attributeValue.ReadUInt16 (); - for (int i = 0; i < namedArgumentCount; i++) { - const byte Field = 0x53; - const byte Property = 0x54; - const byte Boolean = 0x02; - - var memberKind = attributeValue.ReadByte (); - if (memberKind != Field && memberKind != Property) - return false; - - var memberType = attributeValue.ReadByte (); - if (memberType != Boolean) - return false; - - var memberName = attributeValue.ReadSerializedString (); - var memberValue = attributeValue.ReadByte () != 0; - if (memberName == "IsApplication") - return memberValue; - } - return false; - } - void CreateImportFor (string declaringTypeFullName, TypeDefinition type, CodeMemberMethod method, MetadataReader reader, bool hasAlias) { var typeName = reader.GetString (type.Name); From c42f89d7dc2d6e7817469a07ebd359e74932b72f Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 22 May 2026 11:44:29 +0200 Subject: [PATCH 17/25] Clarify System.Type sentinel in attribute decoder Rename the private metadata decoder marker to systemTypeSentinel so it is clear the value is only an identity token for DecodeValue() type checks. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Utilities/DummyCustomAttributeProvider.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/DummyCustomAttributeProvider.cs b/src/Xamarin.Android.Build.Tasks/Utilities/DummyCustomAttributeProvider.cs index 08532fe7a24..278a093e1f5 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/DummyCustomAttributeProvider.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/DummyCustomAttributeProvider.cs @@ -16,31 +16,31 @@ namespace Xamarin.Android.Tasks public class DummyCustomAttributeProvider : ICustomAttributeTypeProvider { public static readonly DummyCustomAttributeProvider Instance = new DummyCustomAttributeProvider (); - static readonly object systemType = new object (); + static readonly object systemTypeSentinel = new object (); public object? GetPrimitiveType (PrimitiveTypeCode typeCode) => null; - public object? GetSystemType () => systemType; + public object? GetSystemType () => systemTypeSentinel; public object? GetSZArrayType (object? elementType) => null; public object? GetTypeFromDefinition (MetadataReader reader, TypeDefinitionHandle handle, byte rawTypeKind) { var type = reader.GetTypeDefinition (handle); - return IsSystemType (reader.GetString (type.Namespace), reader.GetString (type.Name)) ? systemType : null; + return IsSystemType (reader.GetString (type.Namespace), reader.GetString (type.Name)) ? systemTypeSentinel : null; } public object? GetTypeFromReference (MetadataReader reader, TypeReferenceHandle handle, byte rawTypeKind) { var type = reader.GetTypeReference (handle); - return IsSystemType (reader.GetString (type.Namespace), reader.GetString (type.Name)) ? systemType : null; + return IsSystemType (reader.GetString (type.Namespace), reader.GetString (type.Name)) ? systemTypeSentinel : null; } public object? GetTypeFromSerializedName (string name) => name; public PrimitiveTypeCode GetUnderlyingEnumType (object? type) => default (PrimitiveTypeCode); - public bool IsSystemType (object? type) => type == systemType; + public bool IsSystemType (object? type) => type == systemTypeSentinel; static bool IsSystemType (string ns, string name) => ns == "System" && name == "Type"; } From a9ecd4c2a7591fbe00d05734898a3f1183745da8 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 22 May 2026 11:49:44 +0200 Subject: [PATCH 18/25] Revert "Clarify System.Type sentinel in attribute decoder" This reverts commit c42f89d7dc2d6e7817469a07ebd359e74932b72f. --- .../Utilities/DummyCustomAttributeProvider.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/DummyCustomAttributeProvider.cs b/src/Xamarin.Android.Build.Tasks/Utilities/DummyCustomAttributeProvider.cs index 278a093e1f5..08532fe7a24 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/DummyCustomAttributeProvider.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/DummyCustomAttributeProvider.cs @@ -16,31 +16,31 @@ namespace Xamarin.Android.Tasks public class DummyCustomAttributeProvider : ICustomAttributeTypeProvider { public static readonly DummyCustomAttributeProvider Instance = new DummyCustomAttributeProvider (); - static readonly object systemTypeSentinel = new object (); + static readonly object systemType = new object (); public object? GetPrimitiveType (PrimitiveTypeCode typeCode) => null; - public object? GetSystemType () => systemTypeSentinel; + public object? GetSystemType () => systemType; public object? GetSZArrayType (object? elementType) => null; public object? GetTypeFromDefinition (MetadataReader reader, TypeDefinitionHandle handle, byte rawTypeKind) { var type = reader.GetTypeDefinition (handle); - return IsSystemType (reader.GetString (type.Namespace), reader.GetString (type.Name)) ? systemTypeSentinel : null; + return IsSystemType (reader.GetString (type.Namespace), reader.GetString (type.Name)) ? systemType : null; } public object? GetTypeFromReference (MetadataReader reader, TypeReferenceHandle handle, byte rawTypeKind) { var type = reader.GetTypeReference (handle); - return IsSystemType (reader.GetString (type.Namespace), reader.GetString (type.Name)) ? systemTypeSentinel : null; + return IsSystemType (reader.GetString (type.Namespace), reader.GetString (type.Name)) ? systemType : null; } public object? GetTypeFromSerializedName (string name) => name; public PrimitiveTypeCode GetUnderlyingEnumType (object? type) => default (PrimitiveTypeCode); - public bool IsSystemType (object? type) => type == systemTypeSentinel; + public bool IsSystemType (object? type) => type == systemType; static bool IsSystemType (string ns, string name) => ns == "System" && name == "Type"; } From f761140378e34e03c0decae2d08c392813128bbc Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 22 May 2026 11:49:44 +0200 Subject: [PATCH 19/25] Revert "Simplify resource designer attribute decoding" This reverts commit 99877b06b5382b0136b1d58884b9a6cac2c56ccf. --- .../Utilities/DummyCustomAttributeProvider.cs | 21 ++------ .../ResourceDesignerImportGenerator.cs | 52 ++++++++++++++++--- 2 files changed, 50 insertions(+), 23 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/DummyCustomAttributeProvider.cs b/src/Xamarin.Android.Build.Tasks/Utilities/DummyCustomAttributeProvider.cs index 08532fe7a24..a4d5869c42c 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/DummyCustomAttributeProvider.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/DummyCustomAttributeProvider.cs @@ -16,32 +16,21 @@ namespace Xamarin.Android.Tasks public class DummyCustomAttributeProvider : ICustomAttributeTypeProvider { public static readonly DummyCustomAttributeProvider Instance = new DummyCustomAttributeProvider (); - static readonly object systemType = new object (); public object? GetPrimitiveType (PrimitiveTypeCode typeCode) => null; - public object? GetSystemType () => systemType; + public object? GetSystemType () => null; public object? GetSZArrayType (object? elementType) => null; - public object? GetTypeFromDefinition (MetadataReader reader, TypeDefinitionHandle handle, byte rawTypeKind) - { - var type = reader.GetTypeDefinition (handle); - return IsSystemType (reader.GetString (type.Namespace), reader.GetString (type.Name)) ? systemType : null; - } + public object? GetTypeFromDefinition (MetadataReader reader, TypeDefinitionHandle handle, byte rawTypeKind) => null; - public object? GetTypeFromReference (MetadataReader reader, TypeReferenceHandle handle, byte rawTypeKind) - { - var type = reader.GetTypeReference (handle); - return IsSystemType (reader.GetString (type.Namespace), reader.GetString (type.Name)) ? systemType : null; - } + public object? GetTypeFromReference (MetadataReader reader, TypeReferenceHandle handle, byte rawTypeKind) => null; - public object? GetTypeFromSerializedName (string name) => name; + public object? GetTypeFromSerializedName (string name) => null; public PrimitiveTypeCode GetUnderlyingEnumType (object? type) => default (PrimitiveTypeCode); - public bool IsSystemType (object? type) => type == systemType; - - static bool IsSystemType (string ns, string name) => ns == "System" && name == "Type"; + public bool IsSystemType (object? type) => false; } } diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/ResourceDesignerImportGenerator.cs b/src/Xamarin.Android.Build.Tasks/Utilities/ResourceDesignerImportGenerator.cs index 2a85c78ce2a..28d54034a2b 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/ResourceDesignerImportGenerator.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/ResourceDesignerImportGenerator.cs @@ -2,6 +2,7 @@ using System.CodeDom; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Reflection.Metadata; using System.Reflection.PortableExecutable; using Microsoft.Build.Framework; @@ -91,19 +92,56 @@ public void CreateImportMethods (IEnumerable libraries) var attribute = reader.GetCustomAttribute (handle); var fullName = reader.GetCustomAttributeFullName (attribute, Log); if (fullName == "Android.Runtime.ResourceDesignerAttribute") { - var values = attribute.GetCustomAttributeArguments (); - foreach (var arg in values.NamedArguments) { - // application resource IDs are constants, cannot merge. - if (arg.Name == "IsApplication" && arg.Value is bool isApplication && isApplication) { - return null; - } + // application resource IDs are constants, cannot merge. + if (IsApplicationResourceDesigner (reader.GetBlobReader (attribute.Value))) { + return null; } - return values.FixedArguments.Length > 0 ? values.FixedArguments [0].Value as string : null; + return GetResourceDesignerTypeName (reader.GetBlobReader (attribute.Value)); } } return null; } + static string? GetResourceDesignerTypeName (BlobReader attributeValue) + { + if (attributeValue.ReadUInt16 () != 1) + return null; + + // ResourceDesignerAttribute has one fixed argument. In a custom attribute blob, + // both string arguments and System.Type arguments are encoded as a SerString. + // For System.Type in the same assembly, that string is namespace-qualified + // instead of assembly-qualified, matching the resource designer type name. + return attributeValue.ReadSerializedString (); + } + + static bool IsApplicationResourceDesigner (BlobReader attributeValue) + { + if (attributeValue.ReadUInt16 () != 1) + return false; + + attributeValue.ReadSerializedString (); + var namedArgumentCount = attributeValue.ReadUInt16 (); + for (int i = 0; i < namedArgumentCount; i++) { + const byte Field = 0x53; + const byte Property = 0x54; + const byte Boolean = 0x02; + + var memberKind = attributeValue.ReadByte (); + if (memberKind != Field && memberKind != Property) + return false; + + var memberType = attributeValue.ReadByte (); + if (memberType != Boolean) + return false; + + var memberName = attributeValue.ReadSerializedString (); + var memberValue = attributeValue.ReadByte () != 0; + if (memberName == "IsApplication") + return memberValue; + } + return false; + } + void CreateImportFor (string declaringTypeFullName, TypeDefinition type, CodeMemberMethod method, MetadataReader reader, bool hasAlias) { var typeName = reader.GetString (type.Name); From 3b800f38e0190f78c33c2171cad564f57aa54b9f Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 22 May 2026 12:20:23 +0200 Subject: [PATCH 20/25] Expect legacy resource designer warnings in NativeAOT test Document that legacy ResourceDesignerAttribute string values may be namespace-qualified and suppress the expected IL2026 in the AndroidX NativeAOT code-behind test until transitive binding packages rebuild with the Type constructor. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Mono.Android/Android.Runtime/ResourceDesignerAttribute.cs | 2 ++ .../Tests/Xamarin.Android.Build.Tests/CodeBehindTests.cs | 3 +++ 2 files changed, 5 insertions(+) diff --git a/src/Mono.Android/Android.Runtime/ResourceDesignerAttribute.cs b/src/Mono.Android/Android.Runtime/ResourceDesignerAttribute.cs index 89cb5a56978..055a6e9dc1a 100644 --- a/src/Mono.Android/Android.Runtime/ResourceDesignerAttribute.cs +++ b/src/Mono.Android/Android.Runtime/ResourceDesignerAttribute.cs @@ -18,6 +18,8 @@ public ResourceDesignerAttribute ( provider = new TypeResourceTypeProvider (resourceType); } + // Legacy bindings can pass namespace-qualified names such as "Xamarin.Kotlin.Resource", + // so the string value cannot satisfy DynamicallyAccessedMembers unless it is assembly-qualified. [RequiresUnreferencedCode (UseResourceTypeConstructor)] public ResourceDesignerAttribute ( [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicMethods)] diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/CodeBehindTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/CodeBehindTests.cs index 2b55531d7e7..1acfc462314 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/CodeBehindTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/CodeBehindTests.cs @@ -538,6 +538,9 @@ string[] GetBuildProperties (LocalBuilder builder, AndroidRuntime runtime, bool Environment.GetEnvironmentVariable ("RUNNINGONCI"); if (runningOnCI == "true") { Console.WriteLine ("CodeBehindTests: using NativeAOT and running on CI, disabling warnings."); + // Transitive AndroidX bindings can still reference ResourceDesignerAttribute(string); + // this is expected until those libraries rebuild with the Type constructor. + noWarn.Add ("IL2026"); noWarn.Add ("IL2091"); noWarn.Add ("IL2104"); noWarn.Add ("IL3053"); From 677666569fef11b60048617cce496f28590ed157 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 22 May 2026 14:52:19 +0200 Subject: [PATCH 21/25] Drop the Type constructor in favor of AQN type name constructor --- .../ResourceDesignerAttribute.cs | 72 +++++++++---------- 1 file changed, 32 insertions(+), 40 deletions(-) diff --git a/src/Mono.Android/Android.Runtime/ResourceDesignerAttribute.cs b/src/Mono.Android/Android.Runtime/ResourceDesignerAttribute.cs index 055a6e9dc1a..79c03db4241 100644 --- a/src/Mono.Android/Android.Runtime/ResourceDesignerAttribute.cs +++ b/src/Mono.Android/Android.Runtime/ResourceDesignerAttribute.cs @@ -7,60 +7,52 @@ namespace Android.Runtime [AttributeUsage (AttributeTargets.Assembly)] public class ResourceDesignerAttribute : Attribute { - const string UseResourceTypeConstructor = "Resource designer lookup by name requires unreferenced code. Use ResourceDesignerAttribute(Type) instead."; - - IResourceTypeProvider provider; - - public ResourceDesignerAttribute ( - [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicMethods)] - Type resourceType) - { - provider = new TypeResourceTypeProvider (resourceType); - } - - // Legacy bindings can pass namespace-qualified names such as "Xamarin.Kotlin.Resource", - // so the string value cannot satisfy DynamicallyAccessedMembers unless it is assembly-qualified. - [RequiresUnreferencedCode (UseResourceTypeConstructor)] public ResourceDesignerAttribute ( [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicMethods)] string fullName) { - provider = new StringResourceTypeProvider (fullName); + FullName = fullName; } + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicMethods)] public string FullName { - get => provider.FullName; - [RequiresUnreferencedCode (UseResourceTypeConstructor)] - set => provider = new StringResourceTypeProvider (value); + get; + set + { + resourceType = null; + field = value; + } } - [return: DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicMethods)] - internal Type? GetResourceTypeFromAssembly (Assembly assembly) => provider.GetResourceTypeFromAssembly (assembly); - - public bool IsApplication { get; set; } + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicMethods)] + private Type? resourceType; - private interface IResourceTypeProvider - { - [return: DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicMethods)] - Type? GetResourceTypeFromAssembly (Assembly assembly); - string FullName { get; } - } - - private sealed class TypeResourceTypeProvider([DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicMethods)] Type resourceType) : IResourceTypeProvider - { - [return: DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicMethods)] - public Type? GetResourceTypeFromAssembly (Assembly assembly) => assembly == resourceType.Assembly ? resourceType : null; - public string FullName => resourceType.FullName ?? resourceType.Name; - } - - [RequiresUnreferencedCode (UseResourceTypeConstructor)] - private sealed class StringResourceTypeProvider(string fullName) : IResourceTypeProvider + [return: DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicMethods)] + internal Type? GetResourceTypeFromAssembly (Assembly assembly) { + resourceType ??= Type.GetType (FullName, throwOnError: false); + if (resourceType is not null) + { + if (resourceType.Assembly == assembly) { + return resourceType; + } else { + return null; // no need to fallback to the assembly lookup if the type is found but in a different assembly + } + } + + // Fallback for when the type name is not an assembly-qualified name. If an AQN is passed to the constructor, + // the trimmer will report the following warning: + // + // warning IL2122: Type 'XYZ' is not assembly qualified. Type name strings used for dynamically accessing a type should be assembly qualified + // + // Since there is already a build warning, we can suppress the fallback warning. + [UnconditionalSuppressMessage ("Trimming", "IL2026", Justification = "Fallback for non-assembly-qualified type names. Warning is already emitted for non-AQN type names used in the constructor.")] + [UnconditionalSuppressMessage ("Trimming", "IL2073", Justification = "Fallback for non-assembly-qualified type names. Warning is already emitted for non-AQN type names used in the constructor.")] [return: DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicMethods)] - public Type? GetResourceTypeFromAssembly (Assembly assembly) => assembly.GetType (fullName); + static Type? FallbackAssemblyGetType (Assembly a, string name) => a.GetType (name); - public string FullName => fullName; + return FallbackAssemblyGetType (assembly, FullName); } } } From 7c625398a007addbcb9b6541f84ed57aa4bcb169 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 22 May 2026 15:11:57 +0200 Subject: [PATCH 22/25] Use proper AQN and drop Type ctor from ResourceDesignerAttribute --- .../ResourceDesignerAttribute.cs | 4 +++- .../PublicAPI/API-35/PublicAPI.Unshipped.txt | 1 - .../API-36.1/PublicAPI.Unshipped.txt | 1 - .../PublicAPI/API-36/PublicAPI.Unshipped.txt | 1 - .../PublicAPI/API-37/PublicAPI.Unshipped.txt | 1 - .../MonoDroid.Tuner/LinkDesignerBase.cs | 2 +- .../Tasks/GenerateResourceDesigner.cs | 8 ++++++-- .../CodeBehindTests.cs | 6 +++--- .../Expected/GenerateDesignerFileExpected.cs | 2 +- ...leWithElevenStyleableAttributesExpected.cs | 2 +- ...esignerFileWithLibraryReferenceExpected.cs | 2 +- .../Tasks/ManagedResourceParserTests.cs | 10 +++++++--- .../ResourceDesignerImportGenerator.cs | 20 +++++++++++++++---- .../Xamarin.Android.Common.targets | 2 ++ 14 files changed, 41 insertions(+), 21 deletions(-) diff --git a/src/Mono.Android/Android.Runtime/ResourceDesignerAttribute.cs b/src/Mono.Android/Android.Runtime/ResourceDesignerAttribute.cs index 79c03db4241..ad3909fd77a 100644 --- a/src/Mono.Android/Android.Runtime/ResourceDesignerAttribute.cs +++ b/src/Mono.Android/Android.Runtime/ResourceDesignerAttribute.cs @@ -25,6 +25,8 @@ public string FullName } } + public bool IsApplication { get; set; } + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicMethods)] private Type? resourceType; @@ -41,7 +43,7 @@ public string FullName } } - // Fallback for when the type name is not an assembly-qualified name. If an AQN is passed to the constructor, + // Fallback for when the type name is not an assembly-qualified name. If a non-AQN is passed to the constructor, // the trimmer will report the following warning: // // warning IL2122: Type 'XYZ' is not assembly qualified. Type name strings used for dynamically accessing a type should be assembly qualified diff --git a/src/Mono.Android/PublicAPI/API-35/PublicAPI.Unshipped.txt b/src/Mono.Android/PublicAPI/API-35/PublicAPI.Unshipped.txt index d1d006954a2..4f7e6a01587 100644 --- a/src/Mono.Android/PublicAPI/API-35/PublicAPI.Unshipped.txt +++ b/src/Mono.Android/PublicAPI/API-35/PublicAPI.Unshipped.txt @@ -1,4 +1,3 @@ #nullable enable Android.App.ApplicationAttribute.EnableOnBackInvokedCallback.get -> bool Android.App.ApplicationAttribute.EnableOnBackInvokedCallback.set -> void -Android.Runtime.ResourceDesignerAttribute.ResourceDesignerAttribute(System.Type! resourceType) -> void diff --git a/src/Mono.Android/PublicAPI/API-36.1/PublicAPI.Unshipped.txt b/src/Mono.Android/PublicAPI/API-36.1/PublicAPI.Unshipped.txt index d1d006954a2..4f7e6a01587 100644 --- a/src/Mono.Android/PublicAPI/API-36.1/PublicAPI.Unshipped.txt +++ b/src/Mono.Android/PublicAPI/API-36.1/PublicAPI.Unshipped.txt @@ -1,4 +1,3 @@ #nullable enable Android.App.ApplicationAttribute.EnableOnBackInvokedCallback.get -> bool Android.App.ApplicationAttribute.EnableOnBackInvokedCallback.set -> void -Android.Runtime.ResourceDesignerAttribute.ResourceDesignerAttribute(System.Type! resourceType) -> void diff --git a/src/Mono.Android/PublicAPI/API-36/PublicAPI.Unshipped.txt b/src/Mono.Android/PublicAPI/API-36/PublicAPI.Unshipped.txt index 3e35dc64f8c..7d138d0c17f 100644 --- a/src/Mono.Android/PublicAPI/API-36/PublicAPI.Unshipped.txt +++ b/src/Mono.Android/PublicAPI/API-36/PublicAPI.Unshipped.txt @@ -1972,7 +1972,6 @@ Android.Ranging.Wifi.Rtt.RttRangingParams.IsPeriodicRangingHwFeatureEnabled.get Android.Ranging.Wifi.Rtt.RttRangingParams.RangingUpdateRate.get -> Android.Ranging.Raw.RangingDeviceUpdateRate Android.Ranging.Wifi.Rtt.RttRangingParams.ServiceName.get -> string! Android.Ranging.Wifi.Rtt.RttRangingParams.WriteToParcel(Android.OS.Parcel! dest, Android.OS.ParcelableWriteFlags flags) -> void -Android.Runtime.ResourceDesignerAttribute.ResourceDesignerAttribute(System.Type! resourceType) -> void Android.Security.AdvancedProtection.AdvancedProtectionManager Android.Security.AdvancedProtection.AdvancedProtectionManager.ICallback Android.Security.AdvancedProtection.AdvancedProtectionManager.ICallback.OnAdvancedProtectionChanged(bool enabled) -> void diff --git a/src/Mono.Android/PublicAPI/API-37/PublicAPI.Unshipped.txt b/src/Mono.Android/PublicAPI/API-37/PublicAPI.Unshipped.txt index d1d006954a2..4f7e6a01587 100644 --- a/src/Mono.Android/PublicAPI/API-37/PublicAPI.Unshipped.txt +++ b/src/Mono.Android/PublicAPI/API-37/PublicAPI.Unshipped.txt @@ -1,4 +1,3 @@ #nullable enable Android.App.ApplicationAttribute.EnableOnBackInvokedCallback.get -> bool Android.App.ApplicationAttribute.EnableOnBackInvokedCallback.set -> void -Android.Runtime.ResourceDesignerAttribute.ResourceDesignerAttribute(System.Type! resourceType) -> void diff --git a/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/LinkDesignerBase.cs b/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/LinkDesignerBase.cs index 6e50b10d7d7..515da6d5515 100644 --- a/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/LinkDesignerBase.cs +++ b/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/LinkDesignerBase.cs @@ -48,7 +48,7 @@ protected bool FindResourceDesigner (AssemblyDefinition assembly, bool mainAppli { if (p.Name == "IsApplication" && (bool)p.Argument.Value == (mainApplication ? mainApplication : (bool)p.Argument.Value)) { - designerFullName = attribute.ConstructorArguments[0].Value.ToString (); + designerFullName = ResourceDesignerImportGenerator.GetTypeFullNameFromAssemblyQualifiedName (attribute.ConstructorArguments[0].Value.ToString ()); break; } } diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateResourceDesigner.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateResourceDesigner.cs index ee3125e7b2f..a2161081ac7 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateResourceDesigner.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateResourceDesigner.cs @@ -28,6 +28,9 @@ public class GenerateResourceDesigner : AndroidTask public string? Namespace { get; set; } + [Required] + public string AssemblyName { get; set; } = ""; + [Required] public string ProjectDir { get; set; } = ""; @@ -201,8 +204,9 @@ private void WriteFile (string file, CodeTypeDeclaration resources, string langu unit.Namespaces.Add (ns); var resgenatt = new CodeAttributeDeclaration (new CodeTypeReference ("Android.Runtime.ResourceDesignerAttribute", CodeTypeReferenceOptions.GlobalReference)); - var resourceType = new CodeTypeReference (namespaceName.Length > 0 ? namespaceName + ".Resource" : "Resource", CodeTypeReferenceOptions.GlobalReference); - resgenatt.Arguments.Add (new CodeAttributeArgument (new CodeTypeOfExpression (resourceType))); + var resourceTypeName = namespaceName.Length > 0 ? namespaceName + ".Resource" : "Resource"; + var resourceAssemblyQualifiedName = $"{resourceTypeName}, {AssemblyName}"; + resgenatt.Arguments.Add (new CodeAttributeArgument (new CodePrimitiveExpression (resourceAssemblyQualifiedName))); resgenatt.Arguments.Add (new CodeAttributeArgument ("IsApplication", new CodePrimitiveExpression (IsApplication))); unit.AssemblyCustomAttributes.Add (resgenatt); diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/CodeBehindTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/CodeBehindTests.cs index 1acfc462314..f24b67f3599 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/CodeBehindTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/CodeBehindTests.cs @@ -538,11 +538,11 @@ string[] GetBuildProperties (LocalBuilder builder, AndroidRuntime runtime, bool Environment.GetEnvironmentVariable ("RUNNINGONCI"); if (runningOnCI == "true") { Console.WriteLine ("CodeBehindTests: using NativeAOT and running on CI, disabling warnings."); - // Transitive AndroidX bindings can still reference ResourceDesignerAttribute(string); - // this is expected until those libraries rebuild with the Type constructor. - noWarn.Add ("IL2026"); noWarn.Add ("IL2091"); noWarn.Add ("IL2104"); + // Transitive AndroidX bindings can still reference ResourceDesignerAttribute(string) + // with non-AQN type names; this is expected until those libraries rebuild. + noWarn.Add ("IL2122"); noWarn.Add ("IL3053"); noWarn.Add ("XA1040"); } diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Expected/GenerateDesignerFileExpected.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Expected/GenerateDesignerFileExpected.cs index fd2065e455e..b9eb64c5946 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Expected/GenerateDesignerFileExpected.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Expected/GenerateDesignerFileExpected.cs @@ -8,7 +8,7 @@ // //------------------------------------------------------------------------------ -[assembly: global::Android.Runtime.ResourceDesignerAttribute(typeof(global::Foo.Foo.Resource), IsApplication=true)] +[assembly: global::Android.Runtime.ResourceDesignerAttribute("Foo.Foo.Resource, Foo", IsApplication=true)] namespace Foo.Foo { diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Expected/GenerateDesignerFileWithElevenStyleableAttributesExpected.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Expected/GenerateDesignerFileWithElevenStyleableAttributesExpected.cs index 1dcd3ef462b..51c9a1b307c 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Expected/GenerateDesignerFileWithElevenStyleableAttributesExpected.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Expected/GenerateDesignerFileWithElevenStyleableAttributesExpected.cs @@ -9,7 +9,7 @@ // //------------------------------------------------------------------------------ -[assembly: global::Android.Runtime.ResourceDesignerAttribute(typeof(global::Foo.Foo.Resource), IsApplication=true)] +[assembly: global::Android.Runtime.ResourceDesignerAttribute("Foo.Foo.Resource, Foo", IsApplication=true)] namespace Foo.Foo { diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Expected/GenerateDesignerFileWithLibraryReferenceExpected.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Expected/GenerateDesignerFileWithLibraryReferenceExpected.cs index 921a4eed30e..e8d55e29777 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Expected/GenerateDesignerFileWithLibraryReferenceExpected.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Expected/GenerateDesignerFileWithLibraryReferenceExpected.cs @@ -8,7 +8,7 @@ // //------------------------------------------------------------------------------ -[assembly: global::Android.Runtime.ResourceDesignerAttribute(typeof(global::Foo.Foo.Resource), IsApplication=true)] +[assembly: global::Android.Runtime.ResourceDesignerAttribute("Foo.Foo.Resource, Foo", IsApplication=true)] namespace Foo.Foo { diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/ManagedResourceParserTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/ManagedResourceParserTests.cs index 95d016ae7b2..4ea5c5e4d0d 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/ManagedResourceParserTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/ManagedResourceParserTests.cs @@ -384,6 +384,7 @@ GenerateResourceDesigner CreateTask (string path) task.UseManagedResourceGenerator = true; task.DesignTimeBuild = true; task.Namespace = "Foo.Foo"; + task.AssemblyName = "Foo"; task.NetResgenOutputFile = Path.Combine (Root, path, "Resource.designer.cs"); task.DesignTimeOutputFile = Path.Combine (Root, path, "designtime", "Resource.designer.cs"); task.ProjectDir = Path.Combine (Root, path); @@ -461,7 +462,7 @@ public void GenerateDesignerFileFromRtxt ([Values] bool withLibraryReference, [V } [Test] - public void ResourceDesignerImportGeneratorHandlesTypeResourceDesignerAttribute () + public void ResourceDesignerImportGeneratorHandlesAssemblyQualifiedResourceDesignerAttribute () { var path = Path.Combine ("temp", TestName + " Some Space"); CreateResourceDirectory (path); @@ -470,7 +471,7 @@ public void ResourceDesignerImportGeneratorHandlesTypeResourceDesignerAttribute var libraryPath = Path.Combine (path, "Library"); // BuildLibraryWithResources() generates a library Resource.designer.cs with: - // [assembly: ResourceDesignerAttribute(typeof(Library.Resource), IsApplication=false)] + // [assembly: ResourceDesignerAttribute("Library.Resource, Library", IsApplication=false)] BuildLibraryWithResources (libraryPath, AndroidRuntime.MonoVM); var task = CreateTask (path); @@ -483,7 +484,7 @@ public void ResourceDesignerImportGeneratorHandlesTypeResourceDesignerAttribute Assert.IsTrue (task.Execute (), "Task should have executed successfully."); var designer = File.ReadAllText (task.NetResgenOutputFile); // The import generator can only emit this assignment if it decoded the - // typeof(Library.Resource) attribute argument back to "Library.Resource". + // assembly-qualified attribute argument back to "Library.Resource". StringAssert.Contains ("global::Library.Resource.Animator.slide_in_bottom = global::Foo.Foo.Resource.Animator.slide_in_bottom;", designer); Directory.Delete (Path.Combine (Root, path), recursive: true); } @@ -517,6 +518,7 @@ public void UpdateLayoutIdIsIncludedInDesigner ([Values(true, false)] bool useRt task.UseManagedResourceGenerator = true; task.DesignTimeBuild = true; task.Namespace = "Foo.Foo"; + task.AssemblyName = "Foo"; task.NetResgenOutputFile = Path.Combine (Root, path, "Resource.designer.cs"); task.ProjectDir = Path.Combine (Root, path); task.ResourceDirectory = Path.Combine (Root, path, "res") + Path.DirectorySeparatorChar; @@ -635,6 +637,7 @@ public void CompareAapt2AndManagedParserOutput () task.UseManagedResourceGenerator = true; task.DesignTimeBuild = false; task.Namespace = "MonoAndroidApplication4.MonoAndroidApplication4"; + task.AssemblyName = "MonoAndroidApplication4"; task.NetResgenOutputFile = Path.Combine (Root, path, "Resource.designer.aapt2.cs"); task.ProjectDir = Path.Combine (Root, path); task.CaseMapFile = Path.Combine (Root, path, "case_map.txt"); @@ -754,6 +757,7 @@ int styleable ElevenAttributes_attr09 9 task.UseManagedResourceGenerator = true; task.DesignTimeBuild = true; task.Namespace = "Foo.Foo"; + task.AssemblyName = "Foo"; task.NetResgenOutputFile = Path.Combine (Root, path, "Resource.designer.cs"); task.ProjectDir = Path.Combine (Root, path); task.CaseMapFile = Path.Combine (Root, path, "case_map.txt"); diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/ResourceDesignerImportGenerator.cs b/src/Xamarin.Android.Build.Tasks/Utilities/ResourceDesignerImportGenerator.cs index 28d54034a2b..f1a27bddf11 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/ResourceDesignerImportGenerator.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/ResourceDesignerImportGenerator.cs @@ -84,7 +84,7 @@ public void CreateImportMethods (IEnumerable libraries) string? GetResourceDesignerClass (MetadataReader reader) { // Looking for library assemblies: - // [assembly: Android.Runtime.ResourceDesignerAttribute(typeof(MyLibrary.Resource), IsApplication=false)] + // [assembly: Android.Runtime.ResourceDesignerAttribute("MyLibrary.Resource, MyLibrary", IsApplication=false)] // [assembly: Android.Runtime.ResourceDesignerAttribute("MyLibrary.Resource", IsApplication=false)] var assembly = reader.GetAssemblyDefinition (); @@ -109,9 +109,21 @@ public void CreateImportMethods (IEnumerable libraries) // ResourceDesignerAttribute has one fixed argument. In a custom attribute blob, // both string arguments and System.Type arguments are encoded as a SerString. - // For System.Type in the same assembly, that string is namespace-qualified - // instead of assembly-qualified, matching the resource designer type name. - return attributeValue.ReadSerializedString (); + // New assemblies use an assembly-qualified name, but we compare against metadata + // type full names, so strip the assembly name before matching. + return GetTypeFullNameFromAssemblyQualifiedName (attributeValue.ReadSerializedString ()); + } + + internal static string? GetTypeFullNameFromAssemblyQualifiedName (string? typeName) + { + if (typeName is null || typeName.Length == 0) + return typeName; + + int assemblySeparator = typeName.IndexOf (','); + if (assemblySeparator < 0) + return typeName; + + return typeName.Substring (0, assemblySeparator).Trim (); } static bool IsApplicationResourceDesigner (BlobReader attributeValue) diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets index 539ee59f193..a85285c5142 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets @@ -1048,6 +1048,7 @@ because xbuild doesn't support framework reference assemblies. JavaResgenInputFile="$(_GeneratedPrimaryJavaResgenFile)" RTxtFile="$(IntermediateOutputPath)R.txt" Namespace="$(AndroidResgenNamespace)" + AssemblyName="$(AssemblyName)" ProjectDir="$(ProjectDir)" Resources="@(AndroidResource);@(AndroidBoundLayout)" ResourceDirectory="$(MonoAndroidResourcePrefix)" @@ -1304,6 +1305,7 @@ because xbuild doesn't support framework reference assemblies. JavaResgenInputFile="$(_GeneratedPrimaryJavaResgenFile)" RTxtFile="$(IntermediateOutputPath)R.txt" Namespace="$(AndroidResgenNamespace)" + AssemblyName="$(AssemblyName)" ProjectDir="$(ProjectDir)" Resources="@(_AndroidResourceDest)" ResourceDirectory="$(MonoAndroidResDirIntermediate)" From 0b73eb5660ad8da14c47fe0d8bd867b37d53123b Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 22 May 2026 15:17:59 +0200 Subject: [PATCH 23/25] Simplify --- .../ResourceDesignerImportGenerator.cs | 54 ++++--------------- 1 file changed, 9 insertions(+), 45 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/ResourceDesignerImportGenerator.cs b/src/Xamarin.Android.Build.Tasks/Utilities/ResourceDesignerImportGenerator.cs index f1a27bddf11..84bdd57fac6 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/ResourceDesignerImportGenerator.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/ResourceDesignerImportGenerator.cs @@ -92,31 +92,23 @@ public void CreateImportMethods (IEnumerable libraries) var attribute = reader.GetCustomAttribute (handle); var fullName = reader.GetCustomAttributeFullName (attribute, Log); if (fullName == "Android.Runtime.ResourceDesignerAttribute") { - // application resource IDs are constants, cannot merge. - if (IsApplicationResourceDesigner (reader.GetBlobReader (attribute.Value))) { - return null; + var values = attribute.GetCustomAttributeArguments (); + foreach (var arg in values.NamedArguments) { + // application resource IDs are constants, cannot merge. + if (arg.Name == "IsApplication" && arg.Value is bool isApplication && isApplication) { + return null; + } } - return GetResourceDesignerTypeName (reader.GetBlobReader (attribute.Value)); + var typeName = (string?) values.FixedArguments.First ().Value; + return GetTypeFullNameFromAssemblyQualifiedName (typeName); } } return null; } - static string? GetResourceDesignerTypeName (BlobReader attributeValue) - { - if (attributeValue.ReadUInt16 () != 1) - return null; - - // ResourceDesignerAttribute has one fixed argument. In a custom attribute blob, - // both string arguments and System.Type arguments are encoded as a SerString. - // New assemblies use an assembly-qualified name, but we compare against metadata - // type full names, so strip the assembly name before matching. - return GetTypeFullNameFromAssemblyQualifiedName (attributeValue.ReadSerializedString ()); - } - internal static string? GetTypeFullNameFromAssemblyQualifiedName (string? typeName) { - if (typeName is null || typeName.Length == 0) + if (typeName is null) return typeName; int assemblySeparator = typeName.IndexOf (','); @@ -126,34 +118,6 @@ public void CreateImportMethods (IEnumerable libraries) return typeName.Substring (0, assemblySeparator).Trim (); } - static bool IsApplicationResourceDesigner (BlobReader attributeValue) - { - if (attributeValue.ReadUInt16 () != 1) - return false; - - attributeValue.ReadSerializedString (); - var namedArgumentCount = attributeValue.ReadUInt16 (); - for (int i = 0; i < namedArgumentCount; i++) { - const byte Field = 0x53; - const byte Property = 0x54; - const byte Boolean = 0x02; - - var memberKind = attributeValue.ReadByte (); - if (memberKind != Field && memberKind != Property) - return false; - - var memberType = attributeValue.ReadByte (); - if (memberType != Boolean) - return false; - - var memberName = attributeValue.ReadSerializedString (); - var memberValue = attributeValue.ReadByte () != 0; - if (memberName == "IsApplication") - return memberValue; - } - return false; - } - void CreateImportFor (string declaringTypeFullName, TypeDefinition type, CodeMemberMethod method, MetadataReader reader, bool hasAlias) { var typeName = reader.GetString (type.Name); From 50316cd1b7f5fb619fc2d90b062f358e1e4b75d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Rozs=C3=ADval?= Date: Fri, 22 May 2026 16:55:19 +0200 Subject: [PATCH 24/25] Refactor FullName property and update resource type retrieval Refactored FullName property to use auto-implemented property syntax and updated GetResourceTypeFromAssembly method logic. --- .../ResourceDesignerAttribute.cs | 29 +++++++------------ 1 file changed, 10 insertions(+), 19 deletions(-) diff --git a/src/Mono.Android/Android.Runtime/ResourceDesignerAttribute.cs b/src/Mono.Android/Android.Runtime/ResourceDesignerAttribute.cs index ad3909fd77a..8c45435a8ce 100644 --- a/src/Mono.Android/Android.Runtime/ResourceDesignerAttribute.cs +++ b/src/Mono.Android/Android.Runtime/ResourceDesignerAttribute.cs @@ -15,31 +15,22 @@ public ResourceDesignerAttribute ( } [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicMethods)] - public string FullName - { - get; - set - { - resourceType = null; - field = value; - } - } + public string FullName { get; set; } public bool IsApplication { get; set; } - [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicMethods)] - private Type? resourceType; - [return: DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicMethods)] internal Type? GetResourceTypeFromAssembly (Assembly assembly) { - resourceType ??= Type.GetType (FullName, throwOnError: false); - if (resourceType is not null) - { - if (resourceType.Assembly == assembly) { - return resourceType; - } else { - return null; // no need to fallback to the assembly lookup if the type is found but in a different assembly + // Primary scenario: FullName is an assembly-qualified name + if (FullName.IndexOf (',') > 0) { + var resourceType = Type.GetType (FullName); + if (resourceType is not null) { + if (resourceType.Assembly == assembly) { + return resourceType; + } else { + return null; // no need to fallback to the assembly lookup if the type is found but in a different assembly + } } } From a0facd670a76049de60701b609dbdb124ee63d98 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Sat, 23 May 2026 00:45:40 +0200 Subject: [PATCH 25/25] Test legacy ResourceDesignerAttribute format Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Tasks/ManagedResourceParserTests.cs | 69 ++++++++++++++++--- 1 file changed, 61 insertions(+), 8 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/ManagedResourceParserTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/ManagedResourceParserTests.cs index 4ea5c5e4d0d..aaff26bd489 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/ManagedResourceParserTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/ManagedResourceParserTests.cs @@ -15,6 +15,17 @@ namespace Xamarin.Android.Build.Tests { [TestFixture] [Parallelizable (ParallelScope.Children)] public class ManagedResourceParserTests : BaseTest { + class ResourceDesignerAttributeLibraryProject : DotNetXamarinProject + { + public override string ProjectTypeGuid => ""; + + public ResourceDesignerAttributeLibraryProject () + { + Language = XamarinAndroidProjectLanguage.CSharp; + TargetFramework = "net10.0"; + } + } + const string ValuesXml = @" false @@ -332,6 +343,49 @@ void BuildLibraryWithResources (string path, AndroidRuntime runtime) } } + void BuildLibraryWithResourceDesignerAttribute (string path, string resourceDesignerTypeName) + { + var library = new ResourceDesignerAttributeLibraryProject () { + ProjectName = "Library", + }; + library.Sources.Add (new BuildItem.Source ("Resource.cs") { + TextContent = () => $$""" + [assembly: Android.Runtime.ResourceDesignerAttribute ("{{resourceDesignerTypeName}}", IsApplication=false)] + + namespace Android.Runtime + { + [System.AttributeUsage (System.AttributeTargets.Assembly)] + public class ResourceDesignerAttribute : System.Attribute + { + public ResourceDesignerAttribute (string fullName) + { + FullName = fullName; + } + + public string FullName { get; set; } + + public bool IsApplication { get; set; } + } + } + + namespace Library + { + public partial class Resource + { + public partial class Animator + { + public static int slide_in_bottom; + } + } + } + """ + }); + + using (ProjectBuilder builder = CreateDllBuilder (Path.Combine (Root, path))) { + Assert.IsTrue (builder.Build (library), "Build should have succeeded"); + } + } + void CompareFilesIgnoreRuntimeInfoString (string file1, string file2) { FileAssert.Exists (file1); @@ -461,8 +515,9 @@ public void GenerateDesignerFileFromRtxt ([Values] bool withLibraryReference, [V Directory.Delete (Path.Combine (Root, path), recursive: true); } - [Test] - public void ResourceDesignerImportGeneratorHandlesAssemblyQualifiedResourceDesignerAttribute () + [TestCase ("Library.Resource, Library")] + [TestCase ("Library.Resource")] + public void ResourceDesignerImportGeneratorHandlesResourceDesignerAttributeFormats (string resourceDesignerTypeName) { var path = Path.Combine ("temp", TestName + " Some Space"); CreateResourceDirectory (path); @@ -470,21 +525,19 @@ public void ResourceDesignerImportGeneratorHandlesAssemblyQualifiedResourceDesig Assert.IsTrue (mapTask.Execute (), "Map Task should have executed successfully."); var libraryPath = Path.Combine (path, "Library"); - // BuildLibraryWithResources() generates a library Resource.designer.cs with: - // [assembly: ResourceDesignerAttribute("Library.Resource, Library", IsApplication=false)] - BuildLibraryWithResources (libraryPath, AndroidRuntime.MonoVM); + BuildLibraryWithResourceDesignerAttribute (libraryPath, resourceDesignerTypeName); + var libraryAssemblyPath = Path.Combine (Root, libraryPath, "bin", "Debug", "Library.dll"); var task = CreateTask (path); task.RTxtFile = Path.Combine (Root, path, "R.txt"); File.WriteAllText (task.RTxtFile, Rtxt); task.References = new TaskItem [] { - new TaskItem (Path.Combine (Root, libraryPath, "bin", "Debug", "Library.dll")) + new TaskItem (libraryAssemblyPath) }; Assert.IsTrue (task.Execute (), "Task should have executed successfully."); var designer = File.ReadAllText (task.NetResgenOutputFile); - // The import generator can only emit this assignment if it decoded the - // assembly-qualified attribute argument back to "Library.Resource". + // The import generator can only emit this assignment if it found "Library.Resource". StringAssert.Contains ("global::Library.Resource.Animator.slide_in_bottom = global::Foo.Foo.Resource.Animator.slide_in_bottom;", designer); Directory.Delete (Path.Combine (Root, path), recursive: true); }