Refleksja i atrybuty w C# - System.Reflection, Type, Activator, własne atrybuty
Odczytywanie metadanych w trakcie wykonania, dynamiczne tworzenie obiektów i wywoływanie metod przez refleksję, atrybuty jako żółte karteczki dla kodu, projektowanie własnych atrybutów z AttributeUsage. Ósma część kursu C#.
W poprzednim wpisie Wyjątki i IDisposable w C# opanowałeś obsługę błędów i deterministyczne sprzątanie zasobów. Teraz wchodzimy na poziom meta - kod, który patrzy w lustro i czyta sam siebie. To refleksja (System.Reflection) plus jej naturalny partner: atrybuty - znaczniki przyklejane do klas, metod i właściwości.
W tej części zobaczysz, jak C# pozwala odczytać strukturę swojego własnego kodu w trakcie wykonania, jak dynamicznie tworzyć obiekty i wywoływać metody nieznane w momencie kompilacji oraz jak projektować własne atrybuty zgodnie z konwencjami Microsoftu.
1. Czym jest refleksja #
Co to jest: refleksja to mechanizm odczytywania metadanych typu - jego nazwy, klas bazowych, metod, właściwości, pól, atrybutów - w trakcie wykonania programu. Wszystko, co kompilator zapisał o typie, możesz przeczytać programowo.
Dlaczego istnieje: każdy plik DLL/EXE .NET zawiera opis siebie samego. Dokumentacja Microsoftu mówi to wprost: all .NET assemblies contain a specified set of metadata that describes the types and type members defined in the assembly. Refleksja to API, którym ten opis czytasz - z poziomu Twojego własnego kodu.
Wszystkie narzędzia żyją w przestrzeni nazw System.Reflection. Punktem wyjścia jest klasa Type - meta-opis każdego typu w systemie.
using System;
using System.Reflection;
public class User
{
public string Name { get; set; }
public void Greet() => Console.WriteLine($"Hello, I'm {Name}");
}
// Pobranie Type:
Type userType = typeof(User);
// Alternatywnie, jeśli masz już obiekt:
// Type userType = someUser.GetType();
Console.WriteLine($"Class name: {userType.Name}");
Console.WriteLine("Properties:");
foreach (PropertyInfo prop in userType.GetProperties())
{
Console.WriteLine($"- {prop.Name} ({prop.PropertyType.Name})");
}
Console.WriteLine("Methods:");
foreach (MethodInfo method in userType.GetMethods())
{
Console.WriteLine($"- {method.Name}");
}
typeof kontra GetType #
Dwa sposoby na ten sam Type, ale w innym momencie:
typeof(T) | obj.GetType() | |
|---|---|---|
| Kiedy | przy kompilacji (statyczny) | przy wykonaniu (dynamiczny) |
| Co bierze | nazwę typu jako stałą | rzeczywisty typ instancji |
| Polimorfizm | nie - zawsze T | tak - typ konkretny, nie deklarowany |
Vehicle v = new Car();
Console.WriteLine(typeof(Vehicle)); // System.Vehicle - typ ZMIENNEJ
Console.WriteLine(v.GetType()); // System.Car - typ OBIEKTU
Reguła: gdy znasz typ w momencie pisania → typeof(T). Gdy masz obiekt i chcesz wiedzieć, czym naprawdę jest → obj.GetType().
2. Dynamiczne tworzenie obiektów: Activator.CreateInstance #
Refleksja pozwala nie tylko czytać o kodzie, ale też go wykonywać, nie znając konkretnego typu w trakcie kompilacji. Wyobraź sobie, że nazwa klasy przychodzi ze stringa - z pliku konfiguracyjnego, z bazy, od użytkownika. Dynamiczne tworzenie to robota klasy Activator.
Type userType = typeof(User);
// 1. Stworzenie instancji - zwraca object, bo Activator nie zna T
object dynamicUser = Activator.CreateInstance(userType);
// 2. Dynamiczne ustawienie właściwości
PropertyInfo nameProperty = userType.GetProperty("Name");
nameProperty.SetValue(dynamicUser, "Zygmunt");
// 3. Dynamiczne wywołanie metody
MethodInfo greetMethod = userType.GetMethod("Greet");
greetMethod.Invoke(dynamicUser, null);
// Wypisze: Hello, I'm Zygmunt
Pod spodem Activator.CreateInstance wywołuje bezparametrowy konstruktor typu - jeśli go nie ma, rzuca wyjątek. Dla konstruktorów z parametrami istnieje przeciążenie Activator.CreateInstance(Type, object[] args) przyjmujące tablicę argumentów.
Invoke na metodzie: pierwszy argument to instancja (lub null dla metod statycznych), drugi to tablica object[] z argumentami metody (lub null, gdy metoda nie ma parametrów).
Wada refleksji: koszt #
Refleksja omija standardową ścieżkę kompilatora - JIT nie może jej zoptymalizować, każde wywołanie to dynamiczne wyszukanie w metadanych. Pojedyncze użycie jest niezauważalne, ale w pętli wykonującej miliony operacji - to wymierna strata wydajności.
Zasada ogólna: refleksja jest świetna do pluginów, deserializerów, mapperów ORM (sam EF Core używa jej intensywnie), kontenerów wstrzykiwania zależności i frameworków testów. Do codziennej logiki biznesowej - nie.
3. Atrybuty - żółte karteczki dla kodu #
Co to jest: atrybut to deklaratywna informacja dołączana do kodu - klasy, metody, właściwości, pola, parametru, a nawet całego pliku DLL. Cytat z dokumentacji MS: Attributes provide a powerful way to associate metadata, or declarative information, with code. Po prostu metadane - dodatkowy opis.
Dlaczego istnieją: atrybuty same w sobie nic nie robią. Są jak żółte karteczki Post-it przyklejone do kodu. Ich prawdziwa moc ujawnia się dopiero wtedy, gdy inny kod (zwykle przez refleksję) odczyta tę karteczkę i na nią zareaguje. To pozwala oddzielić logikę (np. walidację, autoryzację, routing) od miejsca, w którym pracuje - kod biznesowy zostaje czysty, a “okablowanie” leży w atrybutach.
Wbudowane atrybuty - już je znasz #
[Obsolete("Use NewMethod instead")]
public void OldMethod() { } // kompilator pokaże ostrzeżenie
[Serializable]
public class Document { } // można serializować
public class User
{
[Required]
public string Email { get; set; } // ASP.NET Core: pole obowiązkowe
[MaxLength(100)]
public string DisplayName { get; set; } // ASP.NET / EF: maksymalna długość
}
[HttpPost("/api/users")]
public IActionResult CreateUser(User user) { ... } // ASP.NET MVC: route
Składnia atrybutów #
Atrybut w kodzie podajesz w nawiasach kwadratowych [] nad deklaracją, której dotyczy:
[Serializable] // sufiks "Attribute" można pominąć
public class Document
{
[Required] // pojedynczy atrybut
[MaxLength(100)] // kolejny pod nim
public string Title { get; set; }
[Required, MaxLength(5000)] // można też oddzielić przecinkami
public string Content { get; set; }
}
Pozycyjne kontra nazwane parametry #
Atrybut może przyjąć dwa typy argumentów:
- Pozycyjne - argumenty konstruktora atrybutu. Obowiązkowe, kolejność istotna, podawane jako pierwsze.
- Nazwane - publiczne właściwości lub pola atrybutu. Opcjonalne, kolejność dowolna, podawane po pozycyjnych w formie
Nazwa = wartość.
// "user32.dll" - pozycyjny (z konstruktora)
// SetLastError, ExactSpelling - nazwane (właściwości klasy DllImportAttribute)
[DllImport("user32.dll", SetLastError = true, ExactSpelling = false)]
extern static void SampleMethod();
4. Tworzenie własnego atrybutu #
Wyobraźmy sobie system, w którym niektóre metody mogą być wywoływane tylko przez użytkowników o określonej roli. Zbudujmy atrybut [RequiresRole("Admin")] plus mechanizm sprawdzania.
Krok 1: Klasa atrybutu #
// Atrybut wolno przykleić tylko do metod
[AttributeUsage(AttributeTargets.Method)]
public class RequiresRoleAttribute : Attribute
{
public string RoleName { get; }
public RequiresRoleAttribute(string roleName)
{
RoleName = roleName;
}
}
Trzy konwencje, których trzymamy się zgodnie z MS:
- Klasa dziedziczy po
Attribute- to znak rozpoznawczy, że to atrybut. - Nazwa kończy się sufiksem
Attribute-RequiresRoleAttribute. Przy użyciu kompilator pozwoli pominąć sufiks:[RequiresRole("Admin")]. [AttributeUsage(...)]ogranicza, gdzie atrybut wolno przykleić.AttributeTargets.Methodmówi: tylko nad metodami. Wartości można łączyć przez|:AttributeTargets.Class | AttributeTargets.Struct. ZAllowMultiple = truemożna przyklejać wielokrotnie.
Krok 2: Dekorujemy kod #
public class AdminPanel
{
// Bez atrybutu - dostępna dla każdego
public void ShowStats()
{
Console.WriteLine("Statistics: 42 users online");
}
// Z atrybutem - wymaga roli "Admin"
[RequiresRole("Admin")]
public void DropDatabase()
{
Console.WriteLine("Database dropped! (BOOM)");
}
}
Sama dekoracja nic nie zmienia w działaniu metody. DropDatabase() wywołane bezpośrednio nadal wywoła kod - atrybut to tylko metadana. Potrzebujemy czegoś, co tę metadaną odczyta i na nią zareaguje.
Krok 3: Strażnik z refleksją #
public static class MethodGuard
{
public static void TryExecute(object target, string methodName, string userRole)
{
Type type = target.GetType();
MethodInfo method = type.GetMethod(methodName);
if (method == null) return;
// Odczyt atrybutu - zwraca null, jeśli go nie ma
RequiresRoleAttribute attr = method.GetCustomAttribute<RequiresRoleAttribute>();
if (attr != null && attr.RoleName != userRole)
{
Console.WriteLine($"ERROR: Method requires role '{attr.RoleName}'");
return;
}
method.Invoke(target, null);
}
}
Krok 4: Test #
AdminPanel panel = new AdminPanel();
MethodGuard.TryExecute(panel, "ShowStats", "User");
// → Statistics: 42 users online (atrybutu brak, przepuszczone)
MethodGuard.TryExecute(panel, "DropDatabase", "User");
// → ERROR: Method requires role 'Admin' (atrybut blokuje)
MethodGuard.TryExecute(panel, "DropDatabase", "Admin");
// → Database dropped! (BOOM) (rola się zgadza)
To jest siła kombinacji atrybutów i refleksji: oddzieliliśmy logikę autoryzacji (w MethodGuard) od samej metody (DropDatabase). Kod biznesowy zostaje czysty - nie ma w nim if-ów sprawdzających role. Decyzja “kto może” jest deklaratywna, leży w atrybucie na metodzie.
Identyczna technika stoi za:
- ASP.NET MVC:
[Authorize(Roles = "Admin")]blokuje endpoint - xUnit / NUnit:
[Fact]/[Test]oznacza metodę jako test - Entity Framework:
[Key],[Required],[MaxLength]konfigurują schemat - Newtonsoft.Json / System.Text.Json:
[JsonPropertyName("xyz")]mapuje nazwy pól
5. Pułapki produkcyjne #
Refleksja nie wykrywa private ze zwykłym GetMethod #
Domyślnie Type.GetMethod("Name") szuka tylko metod publicznych instancyjnych. Żeby dosięgnąć private, static lub dziedziczonych, podajesz BindingFlags:
MethodInfo m = type.GetMethod(
"SecretMethod",
BindingFlags.NonPublic | BindingFlags.Instance);
Wyjątki w Invoke są opakowane w TargetInvocationException #
Gdy metoda wywołana przez refleksję rzuci wyjątek, dostajesz go opakowanego w TargetInvocationException. Oryginalny wyjątek leży w InnerException:
try
{
method.Invoke(target, null);
}
catch (TargetInvocationException ex)
{
// ex.InnerException to ten "prawdziwy" wyjątek
Console.WriteLine(ex.InnerException?.Message);
}
Wydajność: cachuj MethodInfo i PropertyInfo #
GetType().GetMethod("X") wykonuje wyszukanie w metadanych za każdym razem. Jeśli wywołujesz wielokrotnie tę samą metodę przez refleksję, cachuj wynik:
private static readonly MethodInfo _cachedMethod =
typeof(SomeClass).GetMethod(nameof(SomeClass.SomeMethod));
Dla naprawdę “gorącej” ścieżki istnieje też możliwość kompilacji wyrażeń Expression<T> do delegata - praktycznie z prędkością zwykłego wywołania. To zaawansowana technika spotkanaprawie wyłącznie w wysokowydajnych frameworkach (np. AutoMapper).
Podsumowanie tematu #
W tej części kursu poznałeś:
- Refleksja - czytanie metadanych typu w trakcie wykonania przez
System.Reflection;Typejako punkt wyjścia typeof(T)kontraobj.GetType()- pierwsze statyczne (kompilacja), drugie dynamiczne (typ obiektu w trakcie wykonania)Activator.CreateInstance(type)- dynamicznenewdla typu poznanego dopiero w trakcie wykonaniaMethodInfo.Invoke+PropertyInfo.SetValue/GetValue- dynamiczne wywołanie metody i ustawianie wartości właściwości- Atrybuty - deklaratywne metadane przyklejane do kodu, same nic nie robią - dopiero w połączeniu z refleksją
- Konwencje atrybutów - sufiks
Attribute, dziedziczenie poAttribute,[AttributeUsage(AttributeTargets.X)]na klasie atrybutu, pozycyjne (konstruktor) i nazwane (właściwości) parametry - Praktyczna kombinacja - dekorujemy metody, refleksja czyta atrybuty, oddzielamy logikę (auth, walidacja, routing) od kodu biznesowego
- Pułapki - BindingFlags dla nie-publicznych,
TargetInvocationExceptionzawija prawdziwy wyjątek, kosztowność refleksji wymaga cache’owania w gorących ścieżkach
W następnym wpisie Architektura pluginów w C# zobaczysz najpotężniejsze zastosowanie refleksji w praktyce - ładowanie kodu z plików .dll, których Twoja aplikacja nigdy wcześniej nie widziała. To wzorzec stojący za rozszerzeniami Visual Studio, skryptami w Unity, modami w grach i auto-discovery kontrolerów w ASP.NET Core.
Quiz i zadanie poniżej. Zadanie łączy oba tematy z dzisiejszego wpisu: własny atrybut z AttributeUsage plus MethodGuard używający refleksji do odczytania i interpretacji tego atrybutu. To dokładnie ten sam wzorzec, który stoi za [Authorize] w ASP.NET Core.
Podobne wpisy
Architektura pluginów w C# - Assembly, dynamiczne ładowanie DLL, AssemblyLoadContext
Praktyczne zastosowanie refleksji - aplikacja rozszerzalna o pluginy ładowane z plików DLL w trakcie wykonania. Assembly, Assembly.LoadFrom, AssemblyLoadContext, izolacja wersji oraz pułapki produkcyjne. Dziewiąta część kursu C#.
Więzy integralności w SQL - Primary Key, Foreign Key, CASCADE, UNIQUE, CHECK
Mechanizmy obronne bazy danych - Primary Key (INT IDENTITY kontra GUID), Foreign Key i integralność referencyjna, trzy strategie ON DELETE (CASCADE/SET NULL/NO ACTION), constraints UNIQUE i CHECK jako walidacja na poziomie bazy. Dwudziesta czwarta część kursu C#.
SQL w praktyce - DDL i DML - CREATE, INSERT, SELECT, UPDATE, DELETE, JOIN
Praktyczny SQL dla programisty C# - podział na DDL/DML/DCL/TCL, CREATE TABLE z kluczami głównymi i obcymi, CRUD (Wielka Czwórka DML), INNER JOIN, klasyczne pułapki UPDATE/DELETE bez WHERE, mapowanie SQL na LINQ w C#. Dwudziesta trzecia część kursu C#.
🔗 Linkują tu