From f49800f73e18b9ecbb33f47e7bc36e45ba7104db Mon Sep 17 00:00:00 2001 From: Florian Rappl Date: Sat, 1 Aug 2026 10:41:12 +0200 Subject: [PATCH 1/4] Remove badges --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index 539b72f..4bcaac2 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,6 @@ [![GitHub Tag](https://img.shields.io/github/tag/AngleSharp/AngleSharp.Js.svg?style=flat-square)](https://github.com/AngleSharp/AngleSharp.Js/releases) [![NuGet Count](https://img.shields.io/nuget/dt/AngleSharp.Js.svg?style=flat-square)](https://www.nuget.org/packages/AngleSharp.Js/) [![Issues Open](https://img.shields.io/github/issues/AngleSharp/AngleSharp.Js.svg?style=flat-square)](https://github.com/AngleSharp/AngleSharp.Js/issues) -[![Gitter Chat](http://img.shields.io/badge/gitter-AngleSharp/AngleSharp-blue.svg?style=flat-square)](https://gitter.im/AngleSharp/AngleSharp) -[![StackOverflow Questions](https://img.shields.io/stackexchange/stackoverflow/t/anglesharp.svg?style=flat-square)](https://stackoverflow.com/tags/anglesharp) [![CLA Assistant](https://cla-assistant.io/readme/badge/AngleSharp/AngleSharp.Js?style=flat-square)](https://cla-assistant.io/AngleSharp/AngleSharp.Js) AngleSharp.Js extends the core AngleSharp library with a .NET-based JavaScript engine. From 91a5a9fca9e112b0fc8a93bc93f260b17faa8063 Mon Sep 17 00:00:00 2001 From: Florian Rappl Date: Tue, 4 Aug 2026 14:34:42 +0200 Subject: [PATCH 2/4] Adjusted versions --- CHANGELOG.md | 6 ++++++ src/AngleSharp.Js.Docs/package.json | 2 +- src/Directory.Build.props | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3fd25ec..89eb5f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +# 1.0.1 + +Released on Wednesday, August 5 2026. + +- tbd + # 1.0.0 Released on Friday, July 31 2026. diff --git a/src/AngleSharp.Js.Docs/package.json b/src/AngleSharp.Js.Docs/package.json index 2e86d1b..44262b2 100644 --- a/src/AngleSharp.Js.Docs/package.json +++ b/src/AngleSharp.Js.Docs/package.json @@ -1,6 +1,6 @@ { "name": "@anglesharp/js", - "version": "0.16.0", + "version": "1.0.1", "preview": true, "description": "The doclet for the AngleSharp.Js documentation.", "keywords": [ diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 2eeb94f..c069bfa 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -10,7 +10,7 @@ entry of CHANGELOG.md, which build/Build.cs passes in as -p:Version, so this value only affects a plain `dotnet build` and no longer needs bumping for a release. --> - 1.0.0 + 1.0.1 From 48935cf37228cf9fbbb2523b616c12a6be36cb56 Mon Sep 17 00:00:00 2001 From: Florian Rappl Date: Tue, 4 Aug 2026 14:40:29 +0200 Subject: [PATCH 3/4] Fixed #136 --- CHANGELOG.md | 2 +- .../DomNameResolutionTests.cs | 59 +++++++++++++++++++ src/AngleSharp.Js.Tests/ScriptEvalTests.cs | 7 +++ src/AngleSharp.Js/Cache/CreatorCache.cs | 6 +- .../Extensions/JsValueExtensions.cs | 2 +- .../Extensions/ReflectionExtensions.cs | 4 +- 6 files changed, 73 insertions(+), 7 deletions(-) create mode 100644 src/AngleSharp.Js.Tests/DomNameResolutionTests.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 89eb5f6..1f8e30b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ Released on Wednesday, August 5 2026. -- tbd +- Fixed `AmbiguousMatchException` in case of multiple `DomName` attributes (#136) # 1.0.0 diff --git a/src/AngleSharp.Js.Tests/DomNameResolutionTests.cs b/src/AngleSharp.Js.Tests/DomNameResolutionTests.cs new file mode 100644 index 0000000..75ee3b4 --- /dev/null +++ b/src/AngleSharp.Js.Tests/DomNameResolutionTests.cs @@ -0,0 +1,59 @@ +namespace AngleSharp.Js.Tests +{ + using AngleSharp.Attributes; + using AngleSharp.Js.Cache; + using NUnit.Framework; + using System; + + [TestFixture] + public class DomNameResolutionTests + { + [Test] + public void TypeOfficialNameWithMultipleDomNamesUsesFirst() + { + var name = typeof(MultiNamedType).GetOfficialName(null); + Assert.AreEqual("PrimaryTypeName", name); + } + + [Test] + public void InterfaceOfficialNameWithMultipleDomNamesUsesFirst() + { + var name = typeof(MultiNamedImplementation).GetOfficialName(null); + Assert.AreEqual("PrimaryInterfaceName", name); + } + + [Test] + public void EnumLiteralDefinitionWithMultipleDomNamesUsesFirst() + { + var definition = typeof(MultiNamedEnum).GetEnumLiteralDefinition(); + Assert.NotNull(definition); + Assert.AreEqual("PrimaryEnumName", definition.Name); + Assert.AreEqual("PrimaryMemberName", definition.Members[0].Name); + } + + [DomName("PrimaryTypeName")] + [DomName("SecondaryTypeName")] + private sealed class MultiNamedType + { + } + + [DomName("PrimaryInterfaceName")] + [DomName("SecondaryInterfaceName")] + private interface IMultiNamedInterface + { + } + + private sealed class MultiNamedImplementation : IMultiNamedInterface + { + } + + [DomName("PrimaryEnumName")] + [DomName("SecondaryEnumName")] + private enum MultiNamedEnum + { + [DomName("PrimaryMemberName")] + [DomName("SecondaryMemberName")] + Value = 1, + } + } +} diff --git a/src/AngleSharp.Js.Tests/ScriptEvalTests.cs b/src/AngleSharp.Js.Tests/ScriptEvalTests.cs index 3d757c0..23258ad 100644 --- a/src/AngleSharp.Js.Tests/ScriptEvalTests.cs +++ b/src/AngleSharp.Js.Tests/ScriptEvalTests.cs @@ -42,6 +42,13 @@ public async Task AccessUndefinedGlobalVariable() Assert.AreEqual("", result); } + [Test] + public async Task EvaluatingSimpleScriptShouldWorkWithMultipleDomNameAttributes_Issue136() + { + var result = await EvaluateComplexScriptAsync(SetResult("(2 + 3).toString()")); + Assert.AreEqual("5", result); + } + [Test] public async Task AccessGlobalVariablesFromOtherScriptShouldWork() { diff --git a/src/AngleSharp.Js/Cache/CreatorCache.cs b/src/AngleSharp.Js/Cache/CreatorCache.cs index 182e717..3b7d9e0 100644 --- a/src/AngleSharp.Js/Cache/CreatorCache.cs +++ b/src/AngleSharp.Js/Cache/CreatorCache.cs @@ -54,7 +54,7 @@ public static EnumLiteralDefinition GetEnumLiteralDefinition(this Type type) if (ti.IsEnum) { - var name = ti.GetCustomAttribute(true)?.OfficialName; + var name = ti.GetCustomAttributes(true).FirstOrDefault()?.OfficialName; if (name != null) { @@ -65,7 +65,7 @@ public static EnumLiteralDefinition GetEnumLiteralDefinition(this Type type) var members = ti.DeclaredFields .Where(m => m.IsLiteral) .Select(m => new EnumLiteralMember( - m.GetCustomAttribute()?.OfficialName, + m.GetCustomAttributes().FirstOrDefault()?.OfficialName, m.GetRawConstantValue())) .Where(m => m.Name != null) .ToArray(); @@ -99,7 +99,7 @@ private static ISet GetNonEnumTypeNames(Assembly assembly) continue; } - var name = ti.GetCustomAttribute(true)?.OfficialName; + var name = ti.GetCustomAttributes(true).FirstOrDefault()?.OfficialName; if (name != null) { diff --git a/src/AngleSharp.Js/Extensions/JsValueExtensions.cs b/src/AngleSharp.Js/Extensions/JsValueExtensions.cs index c6152a3..2085451 100644 --- a/src/AngleSharp.Js/Extensions/JsValueExtensions.cs +++ b/src/AngleSharp.Js/Extensions/JsValueExtensions.cs @@ -115,7 +115,7 @@ public static Object As(this JsValue value, Type targetType, EngineInstance engi .GetTypeInfo() .DeclaredFields .Where(m => m.IsLiteral) - .FirstOrDefault(m => m.GetCustomAttribute()?.OfficialName == literal || m.Name == literal); + .FirstOrDefault(m => m.GetCustomAttributes().FirstOrDefault()?.OfficialName == literal || m.Name == literal); if (member != null) { diff --git a/src/AngleSharp.Js/Extensions/ReflectionExtensions.cs b/src/AngleSharp.Js/Extensions/ReflectionExtensions.cs index 3a309ea..9a4ef2f 100644 --- a/src/AngleSharp.Js/Extensions/ReflectionExtensions.cs +++ b/src/AngleSharp.Js/Extensions/ReflectionExtensions.cs @@ -62,7 +62,7 @@ public static String GetOfficialName(this MemberInfo member) public static String GetOfficialName(this Type currentType, Type baseType) { var ti = currentType.GetTypeInfo(); - var name = ti.GetCustomAttribute(true)?.OfficialName; + var name = ti.GetCustomAttributes(true).FirstOrDefault()?.OfficialName; if (name == null) { @@ -77,7 +77,7 @@ public static String GetOfficialName(this Type currentType, Type baseType) foreach (var impl in interfaces) { - name = impl.GetTypeInfo().GetCustomAttribute(false)?.OfficialName; + name = impl.GetTypeInfo().GetCustomAttributes(false).FirstOrDefault()?.OfficialName; if (name != null) break; From 734b0ea79f5b1f0d9a772b84d14c6ccd98bf01c4 Mon Sep 17 00:00:00 2001 From: Florian Rappl Date: Tue, 4 Aug 2026 15:47:10 +0200 Subject: [PATCH 4/4] Extended fix for #136 --- CHANGELOG.md | 1 + .../DomNameResolutionTests.cs | 82 ++++++++++++ src/AngleSharp.Js/Cache/CreatorCache.cs | 24 ++-- src/AngleSharp.Js/Cache/PrototypeTypeCache.cs | 59 ++++++--- .../Extensions/EngineExtensions.cs | 125 +++++++++++++++--- .../Extensions/ReflectionExtensions.cs | 47 ++++--- 6 files changed, 277 insertions(+), 61 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f8e30b..529b441 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ Released on Wednesday, August 5 2026. - Fixed `AmbiguousMatchException` in case of multiple `DomName` attributes (#136) +- Added support for aliases with multiple `DomName` attributes # 1.0.0 diff --git a/src/AngleSharp.Js.Tests/DomNameResolutionTests.cs b/src/AngleSharp.Js.Tests/DomNameResolutionTests.cs index 75ee3b4..93f506e 100644 --- a/src/AngleSharp.Js.Tests/DomNameResolutionTests.cs +++ b/src/AngleSharp.Js.Tests/DomNameResolutionTests.cs @@ -15,6 +15,13 @@ public void TypeOfficialNameWithMultipleDomNamesUsesFirst() Assert.AreEqual("PrimaryTypeName", name); } + [Test] + public void TypeOfficialNamesWithMultipleDomNamesUsesAll() + { + var names = typeof(MultiNamedType).GetOfficialNames(null); + CollectionAssert.AreEqual(new[] { "PrimaryTypeName", "SecondaryTypeName" }, names); + } + [Test] public void InterfaceOfficialNameWithMultipleDomNamesUsesFirst() { @@ -31,6 +38,55 @@ public void EnumLiteralDefinitionWithMultipleDomNamesUsesFirst() Assert.AreEqual("PrimaryMemberName", definition.Members[0].Name); } + [Test] + public void ConstructorDefinitionWithMultipleDomNamesUsesAll() + { + var definition = typeof(MultiNamedConstructorType).GetConstructorDefinition(); + + Assert.NotNull(definition); + CollectionAssert.AreEqual(new[] { "DOMRect", "SVGRect" }, definition.Names); + Assert.AreEqual("DOMRect", definition.Name); + } + + [Test] + public void ConstructorSelectionPublishesAllAliases() + { + var selected = EngineExtensions.SelectConstructors(new[] { typeof(MultiNamedConstructorType) }); + + Assert.IsTrue(selected.ContainsKey("DOMRect")); + Assert.IsTrue(selected.ContainsKey("SVGRect")); + Assert.AreSame(selected["DOMRect"], selected["SVGRect"]); + Assert.AreEqual(typeof(MultiNamedConstructorType), selected["DOMRect"].Type); + } + + [Test] + public void ConstructorSelectionPrefersFirstClassOverInterfacesForSharedName() + { + var selected = EngineExtensions.SelectConstructors(new[] + { + typeof(IFirstSharedName), + typeof(FirstSharedNameClass), + typeof(SecondSharedNameClass), + typeof(ISecondSharedName), + }); + + Assert.IsTrue(selected.ContainsKey("SharedName")); + Assert.AreEqual(typeof(FirstSharedNameClass), selected["SharedName"].Type); + } + + [Test] + public void ConstructorSelectionUsesFirstInterfaceIfNoClassExists() + { + var selected = EngineExtensions.SelectConstructors(new[] + { + typeof(IFirstSharedName), + typeof(ISecondSharedName), + }); + + Assert.IsTrue(selected.ContainsKey("SharedName")); + Assert.AreEqual(typeof(IFirstSharedName), selected["SharedName"].Type); + } + [DomName("PrimaryTypeName")] [DomName("SecondaryTypeName")] private sealed class MultiNamedType @@ -47,6 +103,32 @@ private sealed class MultiNamedImplementation : IMultiNamedInterface { } + [DomName("DOMRect")] + [DomName("SVGRect")] + private sealed class MultiNamedConstructorType + { + } + + [DomName("SharedName")] + private interface IFirstSharedName + { + } + + [DomName("SharedName")] + private interface ISecondSharedName + { + } + + [DomName("SharedName")] + private sealed class FirstSharedNameClass + { + } + + [DomName("SharedName")] + private sealed class SecondSharedNameClass + { + } + [DomName("PrimaryEnumName")] [DomName("SecondaryEnumName")] private enum MultiNamedEnum diff --git a/src/AngleSharp.Js/Cache/CreatorCache.cs b/src/AngleSharp.Js/Cache/CreatorCache.cs index 3b7d9e0..445bed3 100644 --- a/src/AngleSharp.Js/Cache/CreatorCache.cs +++ b/src/AngleSharp.Js/Cache/CreatorCache.cs @@ -27,13 +27,16 @@ public static ConstructorDefinition GetConstructorDefinition(this Type type) if (!_constructorDefinitions.TryGetValue(type, out var definition)) { var ti = type.GetTypeInfo(); - var names = ti.GetCustomAttributes(); - var name = names.FirstOrDefault(); + var names = ti.GetCustomAttributes() + .Select(m => m.OfficialName) + .Where(m => m != null) + .Distinct(StringComparer.Ordinal) + .ToArray(); - if (name != null && !ti.IsEnum) + if (names.Length > 0 && !ti.IsEnum) { var info = ti.DeclaredConstructors.FirstOrDefault(m => m.GetCustomAttributes().Any()); - definition = new ConstructorDefinition(type, name.OfficialName, info); + definition = new ConstructorDefinition(type, names, info); } _constructorDefinitions.TryAdd(type, definition); @@ -216,10 +219,10 @@ public EnumLiteralMember(String name, Object value) /// sealed class ConstructorDefinition { - public ConstructorDefinition(Type type, String name, ConstructorInfo info) + public ConstructorDefinition(Type type, String[] names, ConstructorInfo info) { Type = type; - Name = name; + Names = names; Info = info; } @@ -229,9 +232,14 @@ public ConstructorDefinition(Type type, String name, ConstructorInfo info) public Type Type { get; } /// - /// Gets the name the constructor is exposed under. + /// Gets the names the constructor is exposed under. /// - public String Name { get; } + public String[] Names { get; } + + /// + /// Gets the primary name of the constructor. + /// + public String Name => Names[0]; /// /// Gets the constructor to invoke, or null if the type cannot be constructed from diff --git a/src/AngleSharp.Js/Cache/PrototypeTypeCache.cs b/src/AngleSharp.Js/Cache/PrototypeTypeCache.cs index de69cd3..70b08cb 100644 --- a/src/AngleSharp.Js/Cache/PrototypeTypeCache.cs +++ b/src/AngleSharp.Js/Cache/PrototypeTypeCache.cs @@ -157,21 +157,23 @@ private static IReadOnlyDictionary CreateDefiningTypes(Assembly as } var baseType = typeInfo.BaseType; - var name = type.GetOfficialName(baseType); + var names = type.GetOfficialNames(baseType); - if (name == null || String.Equals(name, GetNameOf(baseType), StringComparison.Ordinal)) + foreach (var name in names) { - // Either nothing to define, or the base type already defines the very same - // name - so this class is not the topmost one carrying it. - continue; - } + if (String.Equals(name, GetNameOf(baseType), StringComparison.Ordinal)) + { + // The base type already defines this very name, so this class is not + // the topmost one carrying it. + continue; + } - // A name may legitimately be defined twice (col and colgroup are both an - // HTMLTableColElement); share a prototype, but pick the same one every time. - if (!result.TryGetValue(name, out var existing) || - String.CompareOrdinal(type.FullName, existing.FullName) < 0) - { - result[name] = type; + // A name may legitimately be defined twice (col and colgroup are both an + // HTMLTableColElement); share a prototype and keep the first class seen. + if (!result.ContainsKey(name)) + { + result[name] = type; + } } } @@ -202,20 +204,41 @@ private static IReadOnlyDictionary CreateExposedTypes(Assembly ass continue; } - var name = typeInfo.GetCustomAttributes().FirstOrDefault()?.OfficialName; + var names = typeInfo.GetCustomAttributes() + .Select(m => m.OfficialName) + .Where(m => m != null) + .Distinct(StringComparer.Ordinal); + var rank = GetExposureRank(type); - // An interface wins over a class: it is the DOM type, and the class is only - // one way of implementing it. - if (name != null && (!result.TryGetValue(name, out var existing) || - (typeInfo.IsInterface && !existing.GetTypeInfo().IsInterface))) + foreach (var name in names) { - result[name] = type; + if (!result.TryGetValue(name, out var existing) || rank > GetExposureRank(existing)) + { + result[name] = type; + } } } return result; } + private static Int32 GetExposureRank(Type type) + { + var typeInfo = type.GetTypeInfo(); + + if (typeInfo.IsClass) + { + return 2; + } + + if (typeInfo.IsInterface) + { + return 1; + } + + return 0; + } + private static IEnumerable GetLoadableTypes(Assembly assembly) { try diff --git a/src/AngleSharp.Js/Extensions/EngineExtensions.cs b/src/AngleSharp.Js/Extensions/EngineExtensions.cs index f5f1465..c13d3b0 100644 --- a/src/AngleSharp.Js/Extensions/EngineExtensions.cs +++ b/src/AngleSharp.Js/Extensions/EngineExtensions.cs @@ -11,6 +11,7 @@ namespace AngleSharp.Js using Jint.Runtime.Descriptors; using Jint.Runtime.Interop; using System; + using System.Collections.Generic; using System.Reflection; static class EngineExtensions @@ -174,10 +175,95 @@ public static void AddConstructors(this EngineInstance engine, ObjectInstance ct { foreach (var exportedType in assembly.ExportedTypes) { - engine.AddConstructor(ctx, exportedType); + var enumDefinition = exportedType.GetEnumLiteralDefinition(); + + if (enumDefinition != null) + { + SetEnumLiteral(engine, ctx, enumDefinition); + } + } + + var definitions = SelectConstructors(assembly.ExportedTypes); + + foreach (var definition in definitions) + { + ctx.FastSetProperty(definition.Key, CreateConstructorProperty(engine, definition.Value)); + } + } + + private static void SetEnumLiteral(EngineInstance engine, ObjectInstance obj, EnumLiteralDefinition enumDefinition) + { + var target = obj.Get(enumDefinition.Name) as ObjectInstance; + + if (target == null) + { + target = engine.Jint.Intrinsics.Object.Construct(Array.Empty(), JsValue.Undefined); + obj.FastSetProperty(enumDefinition.Name, new PropertyDescriptor(target, false, true, false)); + } + + foreach (var member in enumDefinition.Members) + { + var constant = JsNumber.Create(Convert.ToDouble(member.Value)); + + target.FastSetProperty(member.Name, new PropertyDescriptor( + constant, + false, + true, + false)); } } + internal static IReadOnlyDictionary SelectConstructors(IEnumerable exportedTypes) + { + var selected = new Dictionary(StringComparer.Ordinal); + + foreach (var exportedType in exportedTypes) + { + var definition = exportedType.GetConstructorDefinition(); + + if (definition == null) + { + continue; + } + + var rank = GetExposureRank(exportedType); + + foreach (var name in definition.Names) + { + if (!selected.TryGetValue(name, out var existing) || rank > existing.Rank) + { + selected[name] = new ConstructorCandidate(definition, rank); + } + } + } + + var result = new Dictionary(selected.Count, StringComparer.Ordinal); + + foreach (var selectedName in selected) + { + result[selectedName.Key] = selectedName.Value.Definition; + } + + return result; + } + + private static Int32 GetExposureRank(Type type) + { + var typeInfo = type.GetTypeInfo(); + + if (typeInfo.IsClass) + { + return 2; + } + + if (typeInfo.IsInterface) + { + return 1; + } + + return 0; + } + public static void AddConstructorFunctions(this EngineInstance engine, ObjectInstance ctx, Assembly assembly) { foreach (var exportedType in assembly.ExportedTypes) @@ -200,24 +286,7 @@ public static void AddConstructor(this EngineInstance engine, ObjectInstance obj if (enumDefinition != null) { - var target = obj.Get(enumDefinition.Name) as ObjectInstance; - - if (target == null) - { - target = engine.Jint.Intrinsics.Object.Construct(Array.Empty(), JsValue.Undefined); - obj.FastSetProperty(enumDefinition.Name, new PropertyDescriptor(target, false, true, false)); - } - - foreach (var member in enumDefinition.Members) - { - var constant = JsNumber.Create(Convert.ToDouble(member.Value)); - - target.FastSetProperty(member.Name, new PropertyDescriptor( - constant, - false, - true, - false)); - } + SetEnumLiteral(engine, obj, enumDefinition); return; } @@ -226,7 +295,10 @@ public static void AddConstructor(this EngineInstance engine, ObjectInstance obj if (definition != null) { - obj.FastSetProperty(definition.Name, CreateConstructorProperty(engine, definition)); + foreach (var name in definition.Names) + { + obj.FastSetProperty(name, CreateConstructorProperty(engine, definition)); + } } } @@ -265,6 +337,19 @@ public ConstructorRequest(EngineInstance instance, ConstructorDefinition definit public ConstructorDefinition Definition { get; } } + private readonly struct ConstructorCandidate + { + public ConstructorCandidate(ConstructorDefinition definition, Int32 rank) + { + Definition = definition; + Rank = rank; + } + + public ConstructorDefinition Definition { get; } + + public Int32 Rank { get; } + } + public static void AddConstructorFunction(this EngineInstance engine, ObjectInstance obj, Type type) { var apply = type.GetConstructorFunctionAction(); diff --git a/src/AngleSharp.Js/Extensions/ReflectionExtensions.cs b/src/AngleSharp.Js/Extensions/ReflectionExtensions.cs index 9a4ef2f..b598fb5 100644 --- a/src/AngleSharp.Js/Extensions/ReflectionExtensions.cs +++ b/src/AngleSharp.Js/Extensions/ReflectionExtensions.cs @@ -60,31 +60,48 @@ public static String GetOfficialName(this MemberInfo member) } public static String GetOfficialName(this Type currentType, Type baseType) + { + return currentType.GetOfficialNames(baseType).FirstOrDefault(); + } + + public static String[] GetOfficialNames(this Type currentType, Type baseType) { var ti = currentType.GetTypeInfo(); - var name = ti.GetCustomAttributes(true).FirstOrDefault()?.OfficialName; + var names = ti.GetCustomAttributes(true) + .Select(m => m.OfficialName) + .Where(m => m != null) + .Distinct(StringComparer.Ordinal) + .ToArray(); - if (name == null) + if (names.Length > 0) { - var interfaces = ti.ImplementedInterfaces; + return names; + } - if (baseType != null) - { - var bi = baseType.GetTypeInfo(); - var exclude = bi.ImplementedInterfaces; - interfaces = interfaces.Except(exclude); - } + var interfaces = ti.ImplementedInterfaces; - foreach (var impl in interfaces) - { - name = impl.GetTypeInfo().GetCustomAttributes(false).FirstOrDefault()?.OfficialName; + if (baseType != null) + { + var bi = baseType.GetTypeInfo(); + var exclude = bi.ImplementedInterfaces; + interfaces = interfaces.Except(exclude); + } - if (name != null) - break; + foreach (var impl in interfaces) + { + names = impl.GetTypeInfo().GetCustomAttributes(false) + .Select(m => m.OfficialName) + .Where(m => m != null) + .Distinct(StringComparer.Ordinal) + .ToArray(); + + if (names.Length > 0) + { + return names; } } - return name; + return Array.Empty(); } public static String GetOfficialName(this Enum value)