Reflection and attributes
In short: Reflection is a program looking at its own types while it runs: properties and methods by name, and attributes are notes it can read.
Problem
The property that ToString forgot
The order's ToString is written by hand. Later, someone adds a property for the discount:
class Order{ public int Number { get; set; } = 1042; public string Customer { get; set; } = "Maria Petrova"; public double Total { get; set; } = 86.40; public int Discount { get; set; } = 10; // added later
public override string ToString() { return "Order[Number=" + Number + ", Customer=" + Customer + ", Total=" + Total + "]"; }}
Console.WriteLine(new Order());Order[Number=1042, Customer=Maria Petrova, Total=86.4]The discount isn't in the output, because nobody remembered to update ToString. With ten classes, this happens all the time.
[Fact] above a test method (in NUnit it's [Test]), xUnit finds it and runs it, even though it has never seen your class. How? Both things work through the same idea: a program can look at its own classes while it runs.The idea
The class as an object
For every type, .NET keeps an object of type Type that describes it: name, parent, interfaces, properties, fields, methods, constructors. Getting to it is called reflection (the program sees itself as in a mirror). The classes for the parts of a type (PropertyInfo, MethodInfo and others) are in System.Reflection. There are three ways to get the Type object:
typeof(Order), when you know the class while you write the code;obj.GetType(), when you have an object (it gives the object's real class, not the type of the variable);Type.GetType("System.Collections.ArrayList"), when you only have the name as text.
Type type = Type.GetType("System.Collections.ArrayList");Console.WriteLine("Name: " + type.FullName);Console.WriteLine("Short name: " + type.Name);Console.WriteLine("Parent: " + type.BaseType.FullName);foreach (Type parent in type.GetInterfaces().OrderBy(t => t.Name)){ Console.WriteLine("Interface: " + parent.FullName);}Name: System.Collections.ArrayListShort name: ArrayListParent: System.ObjectInterface: System.ICloneableInterface: System.Collections.ICollectionInterface: System.Collections.IEnumerableInterface: System.Collections.IListArrayList is the old list with no element type, from before List<T>. If there's no type with that name, Type.GetType doesn't throw: it returns null. The interfaces are sorted by name with OrderBy, because the order of GetInterfaces() isn't guaranteed. The name of a generic class ends with ` and the number of its type parameters: typeof(List<string>).Name is List`1.See it
The properties of any object
GetProperties() gives the public properties of the class as PropertyInfo objects (from System.Reflection). Each one knows its name and type and can read its value from a given object:static void Dump(object obj){ Type type = obj.GetType(); Console.WriteLine(type.Name + ":"); foreach (PropertyInfo property in type.GetProperties()) { Console.WriteLine(" " + property.Name + " (" + property.PropertyType.Name + ") = " + property.GetValue(obj)); }}class Room{ public int Number { get; set; } = 214; public bool SeaView { get; set; } = true;}
Dump(new Order());Dump(new Room());Order: Number (Int32) = 1042 Customer (String) = Maria Petrova Total (Double) = 86.4 Discount (Int32) = 10Room: Number (Int32) = 214 SeaView (Boolean) = TrueDiscount property is already there. In C#, the data of a class is seen from outside as properties, and the fields stay hidden. That's why reflection here, and libraries like System.Text.Json and Entity Framework too, look at the properties. They're public, so nothing needs to be opened up, as with setAccessible in Java. The types have their .NET names: int is Int32, double is Double, bool is Boolean.static void Dump(object obj){ Type type = obj.GetType(); Console.WriteLine(type.Name + ":"); foreach (PropertyInfo property in type.GetProperties()) { Console.WriteLine(" " + property.Name + " (" + property.PropertyType.Name + ") = " + property.GetValue(obj)); }}Step by step, in words
- Dump gets an Order object. Its fields are hidden; from outside you see its properties.
- GetType() gives the Type object that describes the Order class.
- The first line is the short name of the class.
- GetProperties() gives 4 PropertyInfo objects: each one knows the property's name and type.
- GetValue(obj) calls the get of the Number property and returns the value: 1042. Int32 is the .NET name of int.
- Customer (String) = Maria Petrova.
- Total (Double) = 86.4.
- Discount: the property that ToString forgot shows up by itself, because reflection gets all the properties.
- The method knows nothing about Order. For Room it prints Number and SeaView the same way.
More precisely, if you are curious
GetProperties()gives only the public properties, the inherited ones too. You ask for other things withBindingFlags:BindingFlags.Instance | BindingFlags.NonPublicgives the private ones, andBindingFlags.DeclaredOnlyonly those of the class itself, without the inherited ones.- Behind every auto-property (
{ get; set; }), the compiler puts a hidden private field. Here are the fields ofRoom, asked for withGetFields:
Room room = new Room();foreach (FieldInfo field in typeof(Room).GetFields(BindingFlags.Instance | BindingFlags.NonPublic)){ Console.WriteLine(field.Name + " = " + field.GetValue(room));}<Number>k__BackingField = 214<SeaView>k__BackingField = True- .NET doesn't promise the order of the properties. Today's .NET returns them in the order of the class, but code that relies on the order is fragile.
Try it
Whose class?
What will the program print?
class Document{}
class Invoice : Document{}
Document doc = new Invoice();Console.WriteLine(doc.GetType().Name);Console.WriteLine(doc.GetType().BaseType.Name);Console.WriteLine(typeof(Document).BaseType.Name);doc is a variable of type Document, but GetType() gives the real class of the object: Invoice. Its parent (BaseType) is Document. Document doesn't list a parent after :, so its parent is Object (object in C#), the common parent from the inheritance module. For object itself, BaseType is null: there's nothing above it.See it
Object, method and private field by name
class Product{ private string name; private double price;
public Product(string name, double price) { this.name = name; this.price = price; }
public void ApplyDiscount(int percent) { price = price * (100 - percent) / 100; }
private string Label() { return name + ": €" + price; }}Type type = typeof(Product);Product coffee = (Product)Activator.CreateInstance(type, "Coffee", 10.0);
MethodInfo discount = type.GetMethod("ApplyDiscount");discount.Invoke(coffee, new object[] { 20 });
FieldInfo price = type.GetField("price", BindingFlags.Instance | BindingFlags.NonPublic);Console.WriteLine("Price after discount: " + price.GetValue(coffee));price.SetValue(coffee, 9.5);
MethodInfo label = type.GetMethod("Label", BindingFlags.Instance | BindingFlags.NonPublic);Console.WriteLine(label.Invoke(coffee, null));Price after discount: 8Coffee: €9.5Activator.CreateInstance(type, "Coffee", 10.0)finds the constructor by the arguments and calls it. It returns anobject, which is why there's the cast toProduct. You can also find a constructor by the types of its parameters withtype.GetConstructor(new[] { typeof(string), typeof(double) }).GetMethod("ApplyDiscount")finds the public method, andInvoke(object, arguments)calls it oncoffee. The arguments are anobject[]array; with no arguments you passnull.- You look for private things with
BindingFlags.Instance | BindingFlags.NonPublic. Without them,GetField("price")returnsnull. You don't need anything likesetAccessible: once found, the private field can be read and written, and the private method can be called.
C# has no checked exceptions, so Main declares nothing. If the name is wrong, GetField and GetMethod don't throw: they return null.
More precisely, if you are curious
List<string>, created and filled without new List or .Add appearing in the code:Type type = typeof(List<>).MakeGenericType(typeof(string));object list = Activator.CreateInstance(type);MethodInfo add = type.GetMethod("Add");add.Invoke(list, new object[] { "Coffee" });add.Invoke(list, new object[] { "Tea" });Console.WriteLine(type);Console.WriteLine(string.Join(", ", (List<string>)list));System.Collections.Generic.List`1[System.String]Coffee, Teatypeof(List<>) is the generic class without its type parameter, and MakeGenericType makes a List<string> out of it. Unlike Java, in .NET the element type is still there while the program runs: Add on this list takes only a string. If you pass a number to Invoke, it throws:
ArgumentException: Object of type 'System.Int32' cannot be converted to type 'System.String'.Try it
The price that isn't there
NullReferenceException on the third line. Which line has the bug?Pick the line with the bug.
The field is called price, but here it says "prise". To the compiler this is ordinary text, so it says nothing. There's no such field, but GetField doesn't throw: it returns null. The error shows up a line later, when GetValue is called on null:
System.NullReferenceException: Object reference not set to an instance of an object.This is the main price of reflection:
- the compiler doesn't check the names and types, and the editor doesn't find them when you rename something;
- the code is slower than an ordinary call and harder to read;
-
BindingFlags.NonPublicgets around encapsulation: you can break a rule that the class protects.
System.Text.Json. You rarely write it in ordinary code.The idea
Your own attributes
[Obsolete], [Fact], [HttpGet]. In Java the same thing is called an annotation and is written with @. On their own, they do nothing. Code that reads them gives them meaning, often through reflection. You can make your own, too: an attribute is a class that inherits from Attribute. This one puts a label on a property, and the method prints only the properties with a label:[AttributeUsage(AttributeTargets.Property)]class LabelAttribute : Attribute{ public string Text { get; }
public LabelAttribute(string text) { Text = text; }}class Booking{ [Label("Number")] public int Number { get; set; } = 501;
[Label("Guest")] public string Guest { get; set; } = "Ivan Georgiev";
[Label("Nights")] public int Nights { get; set; } = 3;
public string InternalNote { get; set; } = "VIP";}static void PrintLabeled(object obj){ foreach (PropertyInfo property in obj.GetType().GetProperties()) { LabelAttribute label = property.GetCustomAttribute<LabelAttribute>(); if (label != null) { Console.WriteLine(label.Text + ": " + property.GetValue(obj)); } }}Number: 501Guest: Ivan GeorgievNights: 3class LabelAttribute : Attributedeclares an attribute. By convention the name ends inAttribute, and when you use it you write it without that:[Label("Guest")]. The argument in the brackets goes to the constructor, andTextis a read-only property.[AttributeUsage(AttributeTargets.Property)]says where it can go: here, only on properties.[Label]above a method is a compile error (CS0592).- There's nothing like Java's
@Retentionhere: attributes always stay in the compiled program, and reflection sees them. property.GetCustomAttribute<LabelAttribute>()returns the attribute, ornullif the property doesn't have it.InternalNotehas no label and isn't printed.
More precisely, if you are curious
- An attribute can also have properties with
set. You set them by name after the arguments:[Label("Guest", Hint = "first and last name")], ifLabelAttributehas aHintproperty. A property you don't set keeps its value from the class. In Java, the same is@Label(value = "Guest", hint = "…"), withdefaultfor the default value. AttributeTargetsalso hasMethod,Class,Field,Parameterand others, and you can combine several with|. WithAllowMultiple = true, one attribute can appear several times in the same place.IsDefined(typeof(LabelAttribute))only checks whether the attribute is there, without creating it. You'll use it in the next card.
See it
How xUnit finds the tests
[Check] attribute is for methods, and the test class checks the discount from the first lesson of the module (with the old bug price > 100):[AttributeUsage(AttributeTargets.Method)]class CheckAttribute : Attribute{}
class DiscountChecks{ [Check] public void TenPercentOff() { Expect(45.0, Discount.Apply(50, 10)); }
[Check] public void ExtraFivePercentFrom100() { Expect(85.0, Discount.Apply(100, 10)); }
void Expect(double expected, double actual) { if (expected != actual) { throw new Exception("expected " + expected + ", but was " + actual); } }}static void RunChecks(Type type){ int passed = 0; int failed = 0; foreach (MethodInfo method in type.GetMethods().OrderBy(m => m.Name)) { if (!method.IsDefined(typeof(CheckAttribute))) { continue; } object instance = Activator.CreateInstance(type); try { method.Invoke(instance, null); Console.WriteLine("PASS " + method.Name); passed++; } catch (TargetInvocationException e) { Console.WriteLine("FAIL " + method.Name + ": " + e.InnerException.Message); failed++; } } Console.WriteLine("Checks: " + (passed + failed) + ", passed: " + passed + ", failed: " + failed);}FAIL ExtraFivePercentFrom100: expected 85, but was 90PASS TenPercentOffChecks: 2, passed: 1, failed: 1xUnit does the same, only with many more details:
- it gets the public methods of the class with
GetMethods()and sorts them withOrderBy, so the order is always the same; - it skips the ones without the attribute:
IsDefinedchecks whether they have it; - it makes a new object for each test with
Activator.CreateInstance, so the tests don't get in each other's way; - it calls the method with
Invoke. If the test throws,Invokewraps the exception in aTargetInvocationException, and the real one is inInnerException.
GetMethods() also gives the public methods inherited from object (ToString, Equals and others). They don't have the attribute and are skipped. Assert.Equal in xUnit is also a method that throws an exception when the values differ. Exactly like Expect here.
Try it
The checks that disappeared
public, and everything else is the same:What will RunChecks print?
class DiscountChecks{ [Check] void TenPercentOff() { ... }
[Check] void ExtraFivePercentFrom100() { ... }}
RunChecks(typeof(DiscountChecks));public in C# is private. GetMethods() without BindingFlags gives only the public methods and doesn't see a single check. The program compiles and runs, it just doesn't do anything. That's why xUnit and NUnit want test methods to be public. You don't need anything like Java's @Retention here: the attribute is still there, the method just isn't visible.Exercise
Code that works with any class
Solve the tasks in your editor.
1. All the properties. WritePrintProperties(object obj), which prints every property as name = value, including the inherited ones, with the parent's properties first. GetProperties() gives the inherited ones too, but the subclass's come first. So start from obj.GetType(), go up with BaseType until object, and for each class take only its own properties with BindingFlags.Instance | BindingFlags.Public | BindingFlags.DeclaredOnly.class Vehicle{ public string Plate { get; set; } = "CA 1234 BK"; public int Year { get; set; } = 2019;}
class Truck : Vehicle{ public double LoadTons { get; set; } = 12.5;}Plate = CA 1234 BKYear = 2019LoadTons = 12.5ToJson(object obj), which returns the properties as JSON: the names in quotes, text in quotes, numbers and true/false without quotes. Use a List<string> with string.Join, is string and is bool: false.ToString() is False, but JSON needs false. 45.0 in C# prints as 45. (The ready-made version is JsonSerializer.Serialize(obj) from System.Text.Json.)class Ticket{ public int Seat { get; set; } = 14; public string Event { get; set; } = "Concert"; public double Price { get; set; } = 45.0; public bool Vip { get; set; } = false;}{"Seat": 14, "Event": "Concert", "Price": 45, "Vip": false}{"Number": 214, "SeaView": true}[MyTest] and [MyBeforeEach] and a method Run(Type type). For each method with [MyTest]: a new object, then the method with [MyBeforeEach] (if there is one), then the test. If the test throws, print the kind (GetType().Name) and the message of the exception. Try it with these tests:[AttributeUsage(AttributeTargets.Method)]class MyTestAttribute : Attribute{}
[AttributeUsage(AttributeTargets.Method)]class MyBeforeEachAttribute : Attribute{}
class Account{ public int Balance { get; private set; }
public Account(int balance) { Balance = balance; }
public void Withdraw(int amount) { if (amount > Balance) { throw new ArgumentException("Short by €" + (amount - Balance)); } Balance -= amount; }}
class AccountChecks{ private Account account;
[MyBeforeEach] public void Fresh() { account = new Account(100); }
[MyTest] public void Withdraw40() { account.Withdraw(40); if (account.Balance != 60) throw new Exception("expected 60, but was " + account.Balance); }
[MyTest] public void Withdraw70() { account.Withdraw(70); if (account.Balance != 30) throw new Exception("expected 30, but was " + account.Balance); }
[MyTest] public void WithdrawTooMuch() { account.Withdraw(130); }}PASS Withdraw40PASS Withdraw70FAIL WithdrawTooMuch: ArgumentException: Short by €30Tests: 3, passed: 2, failed: 1Test
Check yourself
Related lessons
Section titled "Related lessons"- Builds on: Lists · Lambdas and delegates · LINQ · Tests with xUnit
- Continues in: MVC: model, view, controller · Views with Razor · WebAPI: data for other programs
- Tasks on this topic: Real-world programs