From 97aa80f54983908f07c0588d09f65e63d2c0d30c Mon Sep 17 00:00:00 2001 From: Vladimir Pecanac Date: Sun, 11 Oct 2026 12:54:22 +0200 Subject: [PATCH] Custom attributes: retarget to net10.0, add the reading-API examples - CustomAttributes and TestProject move from net6.0 to net10.0. - Microsoft.NET.Test.Sdk 18.10.1, MSTest.TestAdapter and MSTest.TestFramework 4.5.1, coverlet.collector 10.1.0. - Fix the "atributes" typo in GetAttribute's message. - GetMethods never returns null, so the guard checks Length only. - Drop the unused using headers, use file-scoped namespaces and var. - Add GetAttributeGeneric (GetCustomAttribute), HasAttribute (IsDefined) and CountInheritedAttributes, which reads an inherited attribute through Attribute.GetCustomAttributes and through MemberInfo.GetCustomAttributes. TaskBoard and TeamTaskBoard show that the instance method ignores inherit on a property but not on a method. - Three tests cover the new helpers. --- .../CustomAttributeClasses.cs | 119 ++++++++++-------- .../CustomAttributes/CustomAttributes.csproj | 2 +- .../DeveloperTaskAttribute.cs | 23 ++-- .../CustomAttributes/Enums.cs | 17 +-- .../CustomAttributes/ManagerTaskAttribute.cs | 17 +-- .../CustomAttributes/MyTasks.cs | 35 +++--- .../CustomAttributes/ReviewedAttribute.cs | 6 + .../CustomAttributes/TaskBoard.cs | 12 ++ .../TaskDescriptorAttribute.cs | 21 ++-- .../CustomAttributes/TeamTaskBoard.cs | 10 ++ .../CustomAttributes/YourTasks.cs | 15 +-- .../TestProject/PrintAttributesUnitTest.cs | 32 +++++ .../TestProject/TestProject.csproj | 10 +- 13 files changed, 175 insertions(+), 144 deletions(-) create mode 100644 aspnetcore-features/CustomAttributes/CustomAttributes/ReviewedAttribute.cs create mode 100644 aspnetcore-features/CustomAttributes/CustomAttributes/TaskBoard.cs create mode 100644 aspnetcore-features/CustomAttributes/CustomAttributes/TeamTaskBoard.cs diff --git a/aspnetcore-features/CustomAttributes/CustomAttributes/CustomAttributeClasses.cs b/aspnetcore-features/CustomAttributes/CustomAttributes/CustomAttributeClasses.cs index 2adea39655..351a4bf50a 100644 --- a/aspnetcore-features/CustomAttributes/CustomAttributes/CustomAttributeClasses.cs +++ b/aspnetcore-features/CustomAttributes/CustomAttributes/CustomAttributeClasses.cs @@ -1,74 +1,87 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using System.Text; -using System.Threading.Tasks; - -namespace CustomAttributes +using System.Reflection; + +namespace CustomAttributes; + +public static class CustomAttributeHelper { - public static class CustomAttributeHelper + private static void WriteOnTheConsole(Attribute attributeInstance) { - private static void WriteOnTheConsole(Attribute attributeInstance) - { - var pInfoList = attributeInstance.GetType().GetProperties(BindingFlags.Public | - BindingFlags.Instance | - BindingFlags.DeclaredOnly); + var pInfoList = attributeInstance.GetType().GetProperties(BindingFlags.Public | + BindingFlags.Instance | + BindingFlags.DeclaredOnly); - Console.WriteLine($"\nThe {attributeInstance} attribute:"); + Console.WriteLine($"\nThe {attributeInstance} attribute:"); - foreach (var pInfo in pInfoList) - Console.WriteLine($"The {pInfo.Name} property is: {pInfo.GetValue(attributeInstance)}"); - } + foreach (var pInfo in pInfoList) + Console.WriteLine($"The {pInfo.Name} property is: {pInfo.GetValue(attributeInstance)}"); + } - public static string? GetAttribute(Type desiredType, Type desiredAttribute) - { - var attributeInstance = Attribute.GetCustomAttribute(desiredType, desiredAttribute); + public static string? GetAttribute(Type desiredType, Type desiredAttribute) + { + var attributeInstance = Attribute.GetCustomAttribute(desiredType, desiredAttribute); - if (attributeInstance == null) - Console.WriteLine($"The class {desiredType} does not have atributes."); - else - WriteOnTheConsole(attributeInstance); + if (attributeInstance == null) + Console.WriteLine($"The class {desiredType} does not have attributes."); + else + WriteOnTheConsole(attributeInstance); - return attributeInstance?.ToString(); - } + return attributeInstance?.ToString(); + } + + public static T? GetAttributeGeneric(Type desiredType) where T : Attribute + { + return desiredType.GetCustomAttribute(); + } + + public static bool HasAttribute(Type desiredType) where T : Attribute + { + return desiredType.IsDefined(typeof(T)); + } - public static List GetAttributesOfMethods(Type elementType) + public static List GetAttributesOfMethods(Type elementType) + { + var attributes = new List(); + + var methodInfoList = elementType.GetMethods(BindingFlags.Public | + BindingFlags.Instance | + BindingFlags.DeclaredOnly); + + if (methodInfoList.Length == 0) { - List attributes = new List(); + Console.WriteLine($"The type {elementType} does not have any methods."); + return attributes; + } - var methodInfoList = elementType.GetMethods(BindingFlags.Public | - BindingFlags.Instance | - BindingFlags.DeclaredOnly); + foreach (var methodInfo in methodInfoList) + { + var attributeList = Attribute.GetCustomAttributes(methodInfo, true); - if (methodInfoList == null || methodInfoList.Length == 0) + if (attributeList.Length == 0) { - Console.WriteLine($"The type {elementType} does not have any methods."); - return attributes; + Console.WriteLine($"The {elementType.Name}.{methodInfo.Name} method does not have attributes."); + continue; } - foreach (var methodInfo in methodInfoList) - { - var attributeList = Attribute.GetCustomAttributes(methodInfo, true); + Console.WriteLine($"The {elementType.Name}.{methodInfo.Name} method's attribute:"); - if (attributeList.Length == 0) - { - Console.WriteLine($"The {elementType.Name}.{methodInfo.Name} method does not have attributes."); - continue; - } + foreach (var att in attributeList) + { + WriteOnTheConsole(att); + attributes.Add(methodInfo.Name + "-" + att.ToString()); + } - Console.WriteLine($"The {elementType.Name}.{methodInfo.Name} method's attribute:"); + Console.WriteLine(); + } - foreach (var att in attributeList) - { - WriteOnTheConsole(att); - attributes.Add(methodInfo.Name + "-" + att.ToString()); - } + return attributes; + } - Console.WriteLine(); - } + public static (int FromAttributeClass, int FromMember) CountInheritedAttributes(MemberInfo member) + where T : Attribute + { + var fromAttributeClass = Attribute.GetCustomAttributes(member, true).OfType().Count(); + var fromMember = member.GetCustomAttributes(true).OfType().Count(); - return attributes; - } + return (fromAttributeClass, fromMember); } } diff --git a/aspnetcore-features/CustomAttributes/CustomAttributes/CustomAttributes.csproj b/aspnetcore-features/CustomAttributes/CustomAttributes/CustomAttributes.csproj index 9ad2a07d38..ed9781c223 100644 --- a/aspnetcore-features/CustomAttributes/CustomAttributes/CustomAttributes.csproj +++ b/aspnetcore-features/CustomAttributes/CustomAttributes/CustomAttributes.csproj @@ -2,7 +2,7 @@ Exe - net6.0 + net10.0 enable enable diff --git a/aspnetcore-features/CustomAttributes/CustomAttributes/DeveloperTaskAttribute.cs b/aspnetcore-features/CustomAttributes/CustomAttributes/DeveloperTaskAttribute.cs index 0836c829f8..56c47e8edd 100644 --- a/aspnetcore-features/CustomAttributes/CustomAttributes/DeveloperTaskAttribute.cs +++ b/aspnetcore-features/CustomAttributes/CustomAttributes/DeveloperTaskAttribute.cs @@ -1,20 +1,13 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +namespace CustomAttributes; -namespace CustomAttributes +[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] +public class DeveloperTaskAttribute : Attribute { - [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] - public class DeveloperTaskAttribute : Attribute + public DeveloperTaskAttribute(Priorities priority) { - public DeveloperTaskAttribute(Priorities priority) - { - Priority = priority; - } - - public Priorities Priority { get; set; } - public string? Description { get; set; } + Priority = priority; } + + public Priorities Priority { get; set; } + public string? Description { get; set; } } diff --git a/aspnetcore-features/CustomAttributes/CustomAttributes/Enums.cs b/aspnetcore-features/CustomAttributes/CustomAttributes/Enums.cs index 3b4e53704c..71e7fe53c3 100644 --- a/aspnetcore-features/CustomAttributes/CustomAttributes/Enums.cs +++ b/aspnetcore-features/CustomAttributes/CustomAttributes/Enums.cs @@ -1,15 +1,8 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +namespace CustomAttributes; -namespace CustomAttributes +public enum Priorities { - public enum Priorities - { - High, - Mid, - Low - } + High, + Mid, + Low } diff --git a/aspnetcore-features/CustomAttributes/CustomAttributes/ManagerTaskAttribute.cs b/aspnetcore-features/CustomAttributes/CustomAttributes/ManagerTaskAttribute.cs index 8f49d8a4e1..122263934d 100644 --- a/aspnetcore-features/CustomAttributes/CustomAttributes/ManagerTaskAttribute.cs +++ b/aspnetcore-features/CustomAttributes/CustomAttributes/ManagerTaskAttribute.cs @@ -1,15 +1,8 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +namespace CustomAttributes; -namespace CustomAttributes +[AttributeUsage(AttributeTargets.Method, Inherited = false)] +public class ManagerTaskAttribute : Attribute { - [AttributeUsage(AttributeTargets.Method, Inherited = false)] - public class ManagerTaskAttribute : Attribute - { - public Priorities Priority { get; set; } - public bool NeedsReport { get; set; } - } + public Priorities Priority { get; set; } + public bool NeedsReport { get; set; } } diff --git a/aspnetcore-features/CustomAttributes/CustomAttributes/MyTasks.cs b/aspnetcore-features/CustomAttributes/CustomAttributes/MyTasks.cs index 5c7d4a9bc0..68fd467eed 100644 --- a/aspnetcore-features/CustomAttributes/CustomAttributes/MyTasks.cs +++ b/aspnetcore-features/CustomAttributes/CustomAttributes/MyTasks.cs @@ -1,27 +1,20 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +namespace CustomAttributes; -namespace CustomAttributes +[TaskDescriptor(Name = "The task's name", + Description = "Some descriptions for the task", + NeedsManager = true, + DeveloperCount = 5)] +public class MyTasks { - [TaskDescriptor(Name = "The task's name", - Description = "Some descriptions for the task", - NeedsManager = true, - DeveloperCount = 5)] - public class MyTasks + [DeveloperTask(Priorities.Low)] + [DeveloperTask(Priorities.High, Description = "High level description")] + public void ScheduleMeeting() { - [DeveloperTask(Priorities.Low)] - [DeveloperTask(Priorities.High, Description = "High level description")] - public void ScheduleMeeting() - { - } + } - [ManagerTask(Priority = Priorities.Mid, NeedsReport = true)] - [DeveloperTask(Priorities.High, Description = "High level description")] - public virtual void ScheduleInterview() - { - } + [ManagerTask(Priority = Priorities.Mid, NeedsReport = true)] + [DeveloperTask(Priorities.High, Description = "High level description")] + public virtual void ScheduleInterview() + { } } diff --git a/aspnetcore-features/CustomAttributes/CustomAttributes/ReviewedAttribute.cs b/aspnetcore-features/CustomAttributes/CustomAttributes/ReviewedAttribute.cs new file mode 100644 index 0000000000..f1ca827e90 --- /dev/null +++ b/aspnetcore-features/CustomAttributes/CustomAttributes/ReviewedAttribute.cs @@ -0,0 +1,6 @@ +namespace CustomAttributes; + +[AttributeUsage(AttributeTargets.Method | AttributeTargets.Property)] +public class ReviewedAttribute : Attribute +{ +} diff --git a/aspnetcore-features/CustomAttributes/CustomAttributes/TaskBoard.cs b/aspnetcore-features/CustomAttributes/CustomAttributes/TaskBoard.cs new file mode 100644 index 0000000000..c04eee6367 --- /dev/null +++ b/aspnetcore-features/CustomAttributes/CustomAttributes/TaskBoard.cs @@ -0,0 +1,12 @@ +namespace CustomAttributes; + +public class TaskBoard +{ + [Reviewed] + public virtual int OpenTasks { get; set; } + + [Reviewed] + public virtual void Archive() + { + } +} diff --git a/aspnetcore-features/CustomAttributes/CustomAttributes/TaskDescriptorAttribute.cs b/aspnetcore-features/CustomAttributes/CustomAttributes/TaskDescriptorAttribute.cs index c8842bcae9..36c2f2e813 100644 --- a/aspnetcore-features/CustomAttributes/CustomAttributes/TaskDescriptorAttribute.cs +++ b/aspnetcore-features/CustomAttributes/CustomAttributes/TaskDescriptorAttribute.cs @@ -1,17 +1,10 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +namespace CustomAttributes; -namespace CustomAttributes +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)] +public class TaskDescriptorAttribute : Attribute { - [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)] - public class TaskDescriptorAttribute : Attribute - { - public string? Name { get; set; } - public string? Description { get; set; } - public bool NeedsManager { get; set; } - public int DeveloperCount { get; set; } - } + public string? Name { get; set; } + public string? Description { get; set; } + public bool NeedsManager { get; set; } + public int DeveloperCount { get; set; } } diff --git a/aspnetcore-features/CustomAttributes/CustomAttributes/TeamTaskBoard.cs b/aspnetcore-features/CustomAttributes/CustomAttributes/TeamTaskBoard.cs new file mode 100644 index 0000000000..2520dfbf86 --- /dev/null +++ b/aspnetcore-features/CustomAttributes/CustomAttributes/TeamTaskBoard.cs @@ -0,0 +1,10 @@ +namespace CustomAttributes; + +public class TeamTaskBoard : TaskBoard +{ + public override int OpenTasks { get; set; } + + public override void Archive() + { + } +} diff --git a/aspnetcore-features/CustomAttributes/CustomAttributes/YourTasks.cs b/aspnetcore-features/CustomAttributes/CustomAttributes/YourTasks.cs index cd087945a9..2e17c3a356 100644 --- a/aspnetcore-features/CustomAttributes/CustomAttributes/YourTasks.cs +++ b/aspnetcore-features/CustomAttributes/CustomAttributes/YourTasks.cs @@ -1,16 +1,9 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +namespace CustomAttributes; -namespace CustomAttributes +public class YourTasks : MyTasks { - public class YourTasks : MyTasks + [DeveloperTask(Priorities.Mid, Description = "Mid level description")] + public override void ScheduleInterview() { - [DeveloperTask(Priorities.Mid, Description = "Mid level description")] - public override void ScheduleInterview() - { - } } } diff --git a/aspnetcore-features/CustomAttributes/TestProject/PrintAttributesUnitTest.cs b/aspnetcore-features/CustomAttributes/TestProject/PrintAttributesUnitTest.cs index b9d507ccb5..5134981609 100644 --- a/aspnetcore-features/CustomAttributes/TestProject/PrintAttributesUnitTest.cs +++ b/aspnetcore-features/CustomAttributes/TestProject/PrintAttributesUnitTest.cs @@ -35,5 +35,37 @@ public void WhenCorrectYourTaskInput_ThenCorrectMultipleAttOutput() Assert.IsTrue(result); } + + [TestMethod] + public void WhenGenericGetCustomAttribute_ThenTypedInstanceWithoutCast() + { + var descriptor = CustomAttributeHelper.GetAttributeGeneric(typeof(MyTasks)); + var missing = CustomAttributeHelper.GetAttributeGeneric(typeof(MyTasks)); + + Assert.IsNotNull(descriptor); + Assert.AreEqual("The task's name", descriptor.Name); + Assert.AreEqual(5, descriptor.DeveloperCount); + Assert.IsNull(missing); + } + + [TestMethod] + public void WhenIsDefined_ThenTrueOnlyWhereTheAttributeIsApplied() + { + Assert.IsTrue(CustomAttributeHelper.HasAttribute(typeof(MyTasks))); + Assert.IsFalse(CustomAttributeHelper.HasAttribute(typeof(MyTasks))); + } + + [TestMethod] + public void WhenInheritIsTrue_ThenMemberGetCustomAttributesIgnoresItOnPropertiesOnly() + { + var property = typeof(TeamTaskBoard).GetProperty(nameof(TeamTaskBoard.OpenTasks))!; + var method = typeof(TeamTaskBoard).GetMethod(nameof(TeamTaskBoard.Archive))!; + + var onProperty = CustomAttributeHelper.CountInheritedAttributes(property); + var onMethod = CustomAttributeHelper.CountInheritedAttributes(method); + + Assert.AreEqual((1, 0), onProperty); + Assert.AreEqual((1, 1), onMethod); + } } } \ No newline at end of file diff --git a/aspnetcore-features/CustomAttributes/TestProject/TestProject.csproj b/aspnetcore-features/CustomAttributes/TestProject/TestProject.csproj index a9ecf2ff40..a66bc938ee 100644 --- a/aspnetcore-features/CustomAttributes/TestProject/TestProject.csproj +++ b/aspnetcore-features/CustomAttributes/TestProject/TestProject.csproj @@ -1,17 +1,17 @@ - net6.0 + net10.0 enable false - - - - + + + +