Atrybuty i Refleksja to fundamenty metaprogramowania w C#. W 2015 roku używałeś podstawowych atrybutów i refleksji do introspekcji w czasie wykonywania. W 2026 roku masz Source Generators (generatory źródła) jako alternatywę w czasie kompilacji!
W tym wpisie poznasz wbudowane atrybuty (Obsolete, Conditional, CallerMemberName), nauczymy się tworzyć własne atrybuty, poznamy podstawy refleksji, zrobimy wprowadzenie do Source Generators!
📅 Timeline - ewolucja attributes i reflection
C# 1.0 (2002) - Attributes, Reflection basics
C# 5.0 (2012) - Caller info attributes (CallerMemberName, etc.)
Attribute = specjalny typ który dodaje metadane do elementów kodu (klasy, metody, properties, etc.)
Metadane mogą być czytane w runtime przez Reflection lub w compile-time przez kompilator/Source Generators
// Attribute to klasa dziedzicząca po System.Attribute
// Używasz atrybutów przez [AttributeName]
// Przykład - [Obsolete] attribute
[Obsolete("Ta metoda jest przestarzała, użyj NewMethod()")]
public void OldMethod()
{
Console.WriteLine("Old implementation");
}
// Kompilator pokaże warning gdy ktoś użyje OldMethod!
OldMethod(); // ⚠️ CS0618: 'OldMethod' is obsolete: 'Ta metoda jest przestarzała...'
// Attributes mogą mieć parametry
[Obsolete("Użyj NewMethod()", error: true)] // error: true = błąd kompilacji!
public void VeryOldMethod()
{
Console.WriteLine("Very old");
}
// VeryOldMethod(); // ❌ BŁĄD kompilacji!
Gdzie można używać attributes?
// Attributes można dodać do prawie wszystkiego!
// Class
[Serializable]
public class Person { }
// Method
[Obsolete]
public void DoSomething() { }
// Property
[Required]
public string Name { get; set; }
// Field
[NonSerialized]
private int _tempData;
// Parameter
public void Log([CallerMemberName] string caller = "") { }
// Return value
[return: MarshalAs(UnmanagedType.Bool)]
public bool CheckStatus() { return true; }
// Assembly (w osobnym pliku, zwykle AssemblyInfo.cs)
[assembly: AssemblyVersion("1.0.0.0")]
// Module
[module: SomeAttribute]
Built-in Attributes - najważniejsze
[Obsolete] - deprecation warnings
// [Obsolete] - oznacz kod jako przestarzały
[Obsolete("Użyj GetUserAsync() zamiast tego")]
public User GetUser(int id)
{
return _repository.GetUser(id);
}
// Warning w kompilacji
var user = GetUser(123); // ⚠️ Warning
// [Obsolete] z error: true
[Obsolete("Ta metoda została usunięta w v2.0", error: true)]
public void RemovedMethod()
{
// ...
}
// RemovedMethod(); // ❌ BŁĄD kompilacji
// Praktyczne użycie - migration path
public class UserService
{
// Nowa metoda
public async Task<User> GetUserAsync(int id)
{
return await _repository.GetUserAsync(id);
}
// Stara metoda - deprecated
[Obsolete("Użyj GetUserAsync() dla lepszej wydajności")]
public User GetUser(int id)
{
return GetUserAsync(id).GetAwaiter().GetResult();
}
}
[Conditional] - conditional compilation
// [Conditional] - metoda jest wywoływana TYLKO gdy symbol jest zdefiniowany
using System.Diagnostics;
public class Logger
{
[Conditional("DEBUG")]
public static void LogDebug(string message)
{
Console.WriteLine($"[DEBUG] {message}");
}
[Conditional("TRACE")]
public static void LogTrace(string message)
{
Console.WriteLine($"[TRACE] {message}");
}
}
// W Debug build (DEBUG symbol defined)
Logger.LogDebug("Starting process"); // ✅ Wywoła metodę
// W Release build (DEBUG symbol NOT defined)
Logger.LogDebug("Starting process"); // ❌ Kod jest USUNIĘTY przez kompilator!
// Praktyczne użycie - performance logging
public class PerformanceMonitor
{
[Conditional("PERFORMANCE_LOGGING")]
public static void LogPerformance(string operation, long milliseconds)
{
Console.WriteLine($"{operation} took {milliseconds}ms");
}
}
// W production build - zero overhead (kod usunięty)!
PerformanceMonitor.LogPerformance("Database query", 150);
Caller Info Attributes - metadane o callerze
// Caller info attributes - kompilator automatycznie wypełnia parametry!
using System.Runtime.CompilerServices;
public class Logger
{
public static void Log(
string message,
[CallerMemberName] string memberName = "",
[CallerFilePath] string filePath = "",
[CallerLineNumber] int lineNumber = 0)
{
Console.WriteLine($"[{memberName}] {message}");
Console.WriteLine($" at {filePath}:{lineNumber}");
}
}
// Użycie - nie podajesz parametrów caller info!
public class UserService
{
public void CreateUser(string name)
{
Logger.Log("Creating user");
// Output:
// [CreateUser] Creating user
// at C:\Project\UserService.cs:15
// Kompilator AUTOMATYCZNIE wypełnił:
// memberName = "CreateUser"
// filePath = pełna ścieżka do pliku
// lineNumber = 15
}
}
// Praktyczne użycie - INotifyPropertyChanged
public class Person : INotifyPropertyChanged
{
private string _name;
public string Name
{
get => _name;
set
{
if (_name != value)
{
_name = value;
OnPropertyChanged(); // Bez parametru!
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
// propertyName będzie "Name" automatycznie! ✨
}
}
Inne przydatne built-in attributes
// [Serializable] - klasa może być serializowana
[Serializable]
public class Person
{
public string Name { get; set; }
[NonSerialized] // To pole NIE będzie serializowane
private int _tempData;
}
// [Required] - walidacja (ASP.NET)
public class UserDto
{
[Required(ErrorMessage = "Name jest wymagane")]
public string Name { get; set; }
[Range(18, 100, ErrorMessage = "Age musi być między 18 a 100")]
public int Age { get; set; }
[EmailAddress(ErrorMessage = "Nieprawidłowy email")]
public string Email { get; set; }
}
// [JsonPropertyName] - custom JSON property name
public class Product
{
[JsonPropertyName("product_id")]
public int Id { get; set; }
[JsonPropertyName("product_name")]
public string Name { get; set; }
}
// JSON: { "product_id": 1, "product_name": "Laptop" }
// [DebuggerDisplay] - custom display w debuggerze
[DebuggerDisplay("Person: {Name}, Age: {Age}")]
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
}
// W debuggerze Visual Studio pokaże: "Person: Jan, Age: 30"
// [Flags] - enum jako bit flags
[Flags]
public enum FileAccess
{
Read = 1,
Write = 2,
Execute = 4
}
FileAccess access = FileAccess.Read | FileAccess.Write; // 3
Console.WriteLine(access); // "Read, Write" (bez [Flags] byłoby "3")
Custom Attributes - tworzenie własnych
Definiowanie custom attribute
// Custom attribute - dziedziczy po System.Attribute
// Konwencja: nazwa kończy się na "Attribute"
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class AuthorAttribute : Attribute
{
public string Name { get; }
public string Date { get; set; } // Optional property
public AuthorAttribute(string name)
{
Name = name;
}
}
// Użycie
[Author("Jan Kowalski", Date = "2026-01-15")]
public class Calculator
{
[Author("Anna Nowak")]
public int Add(int a, int b)
{
return a + b;
}
}
// Możesz pominąć "Attribute" suffix
[Author("Jan")] // Równoważne: [AuthorAttribute("Jan")]
public class Example { }
AttributeUsage - gdzie można użyć attribute
// AttributeUsage określa gdzie attribute może być użyty
[AttributeUsage(
AttributeTargets.Class | AttributeTargets.Struct, // Tylko na class i struct
AllowMultiple = false, // Tylko jeden na element
Inherited = true)] // Dziedziczy na derived classes
public class DocumentationAttribute : Attribute
{
public string Description { get; }
public DocumentationAttribute(string description)
{
Description = description;
}
}
// AttributeTargets options:
// - Assembly, Module
// - Class, Struct, Enum, Interface, Delegate
// - Constructor, Method, Property, Field, Event, Parameter, ReturnValue
// - GenericParameter
// - All (wszystkie)
// AllowMultiple = true - możesz użyć wiele razy
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
public class TestCaseAttribute : Attribute
{
public object[] Arguments { get; }
public TestCaseAttribute(params object[] args)
{
Arguments = args;
}
}
// Wiele tego samego attribute
[TestCase(1, 2, 3)]
[TestCase(5, 10, 15)]
[TestCase(-1, -2, -3)]
public void TestSum(int a, int b, int expected)
{
Assert.Equal(expected, a + b);
}
Praktyczne przykłady - custom attributes
// Przykład 1: Validation attribute
[AttributeUsage(AttributeTargets.Property)]
public class MinLengthAttribute : Attribute
{
public int Length { get; }
public string ErrorMessage { get; set; }
public MinLengthAttribute(int length)
{
Length = length;
}
public bool IsValid(string value)
{
return value?.Length >= Length;
}
}
public class User
{
[MinLength(3, ErrorMessage = "Name musi mieć min. 3 znaki")]
public string Name { get; set; }
[MinLength(8, ErrorMessage = "Password musi mieć min. 8 znaków")]
public string Password { get; set; }
}
// Przykład 2: Dependency injection attribute
[AttributeUsage(AttributeTargets.Property)]
public class InjectAttribute : Attribute
{
public string ServiceName { get; set; }
}
public class OrderController
{
[Inject]
public IOrderService OrderService { get; set; }
[Inject(ServiceName = "CachedRepository")]
public IRepository Repository { get; set; }
}
// Przykład 3: API routing attribute
[AttributeUsage(AttributeTargets.Method)]
public class RouteAttribute : Attribute
{
public string Path { get; }
public string Method { get; set; } = "GET";
public RouteAttribute(string path)
{
Path = path;
}
}
public class UsersController
{
[Route("/api/users")]
public List<User> GetUsers() { /* ... */ }
[Route("/api/users/{id}", Method = "POST")]
public User CreateUser(int id) { /* ... */ }
}
// Przykład 4: Caching attribute
[AttributeUsage(AttributeTargets.Method)]
public class CacheAttribute : Attribute
{
public int DurationSeconds { get; set; } = 60;
public string Key { get; set; }
}
public class DataService
{
[Cache(DurationSeconds = 300, Key = "all-users")]
public List<User> GetAllUsers()
{
return _repository.GetUsers();
}
}
Reflection - runtime introspection
Podstawy Reflection - Type i TypeInfo
// Reflection - badanie typów w runtime
using System.Reflection;
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
public void SayHello()
{
Console.WriteLine($"Hello, I'm {Name}");
}
}
// Uzyskanie Type
Type personType = typeof(Person); // Compile-time
Person person = new Person();
Type personType2 = person.GetType(); // Runtime
// Type info
Console.WriteLine(personType.Name); // "Person"
Console.WriteLine(personType.FullName); // "MyNamespace.Person"
Console.WriteLine(personType.IsClass); // true
Console.WriteLine(personType.IsValueType); // false
Reflection - properties i methods
// Czytanie properties
Type personType = typeof(Person);
// GetProperties - wszystkie public properties
PropertyInfo[] properties = personType.GetProperties();
foreach (var prop in properties)
{
Console.WriteLine($"Property: {prop.Name}, Type: {prop.PropertyType}");
}
// Output:
// Property: Name, Type: System.String
// Property: Age, Type: System.Int32
// GetMethods - wszystkie public methods
MethodInfo[] methods = personType.GetMethods();
foreach (var method in methods)
{
Console.WriteLine($"Method: {method.Name}");
}
// Output:
// Method: SayHello
// Method: get_Name (property getter)
// Method: set_Name (property setter)
// Method: get_Age
// Method: set_Age
// Method: GetType (z Object)
// Method: ToString (z Object)
// ... etc.
// Specific method
MethodInfo sayHello = personType.GetMethod("SayHello");
Console.WriteLine(sayHello.ReturnType); // System.Void
Reflection - tworzenie instancji i wywoływanie metod
// Tworzenie instancji przez Reflection
Type personType = typeof(Person);
// Activator.CreateInstance
object personObj = Activator.CreateInstance(personType);
// Cast do Person
Person person = (Person)personObj;
// Ustawianie property values przez Reflection
PropertyInfo nameProp = personType.GetProperty("Name");
nameProp.SetValue(person, "Jan");
PropertyInfo ageProp = personType.GetProperty("Age");
ageProp.SetValue(person, 30);
Console.WriteLine($"{person.Name}, {person.Age}"); // "Jan, 30"
// Wywoływanie metod przez Reflection
MethodInfo sayHelloMethod = personType.GetMethod("SayHello");
sayHelloMethod.Invoke(person, null); // "Hello, I'm Jan"
// Method z parametrami
public class Calculator
{
public int Add(int a, int b)
{
return a + b;
}
}
Type calcType = typeof(Calculator);
object calc = Activator.CreateInstance(calcType);
MethodInfo addMethod = calcType.GetMethod("Add");
object result = addMethod.Invoke(calc, new object[] { 5, 3 });
Console.WriteLine(result); // 8
Reflection - czytanie attributes
// Czytanie attributes przez Reflection
[Author("Jan Kowalski", Date = "2026-01-15")]
public class Calculator
{
[Obsolete("Użyj AddNumbers()")]
public int Add(int a, int b)
{
return a + b;
}
public int AddNumbers(int a, int b)
{
return a + b;
}
}
Type calcType = typeof(Calculator);
// Sprawdź czy typ ma attribute
bool hasAuthor = calcType.IsDefined(typeof(AuthorAttribute), false);
Console.WriteLine(hasAuthor); // true
// Pobierz attribute
AuthorAttribute author = calcType.GetCustomAttribute<AuthorAttribute>();
Console.WriteLine($"Author: {author.Name}, Date: {author.Date}");
// Output: "Author: Jan Kowalski, Date: 2026-01-15"
// Attributes z metod
MethodInfo addMethod = calcType.GetMethod("Add");
ObsoleteAttribute obsolete = addMethod.GetCustomAttribute<ObsoleteAttribute>();
if (obsolete != null)
{
Console.WriteLine($"Method is obsolete: {obsolete.Message}");
// Output: "Method is obsolete: Użyj AddNumbers()"
}
// Wszystkie attributes
Attribute[] allAttributes = Attribute.GetCustomAttributes(calcType);
foreach (var attr in allAttributes)
{
Console.WriteLine(attr.GetType().Name);
}
Praktyczne przykłady - Reflection
// Przykład 1: Simple dependency injection container
public class Container
{
private Dictionary<Type, Type> _registrations = new();
public void Register<TInterface, TImplementation>()
{
_registrations[typeof(TInterface)] = typeof(TImplementation);
}
public T Resolve<T>()
{
Type implementationType = _registrations[typeof(T)];
return (T)Activator.CreateInstance(implementationType);
}
}
// Użycie
var container = new Container();
container.Register<ILogger, ConsoleLogger>();
ILogger logger = container.Resolve<ILogger>();
logger.Log("Message");
// Przykład 2: Property mapper
public static void MapProperties<TSource, TDest>(TSource source, TDest dest)
{
Type sourceType = typeof(TSource);
Type destType = typeof(TDest);
foreach (var sourceProp in sourceType.GetProperties())
{
var destProp = destType.GetProperty(sourceProp.Name);
if (destProp != null && destProp.CanWrite)
{
object value = sourceProp.GetValue(source);
destProp.SetValue(dest, value);
}
}
}
// Użycie
var person = new Person { Name = "Jan", Age = 30 };
var dto = new PersonDto();
MapProperties(person, dto);
Console.WriteLine($"{dto.Name}, {dto.Age}"); // "Jan, 30"
// Przykład 3: Validation framework
public static bool ValidateObject(object obj)
{
Type type = obj.GetType();
foreach (var prop in type.GetProperties())
{
var requiredAttr = prop.GetCustomAttribute<RequiredAttribute>();
if (requiredAttr != null)
{
object value = prop.GetValue(obj);
if (value == null || (value is string str && string.IsNullOrEmpty(str)))
{
Console.WriteLine($"{prop.Name} is required!");
return false;
}
}
var minLengthAttr = prop.GetCustomAttribute<MinLengthAttribute>();
if (minLengthAttr != null && prop.GetValue(obj) is string strValue)
{
if (!minLengthAttr.IsValid(strValue))
{
Console.WriteLine(minLengthAttr.ErrorMessage);
return false;
}
}
}
return true;
}
// Użycie
var user = new User { Name = "Jan" };
bool isValid = ValidateObject(user);
⚠️ Reflection - performance warning!
❌ Reflection jest WOLNE - 10-100x wolniejsze niż direct code
❌ Brak compile-time safety - błędy dopiero w runtime
❌ Trudniejszy debugging i maintenance
✅ Używaj Reflection tylko gdy MUSISZ (frameworks, tools, dynamic scenarios)
✅ Cachuj Type/MethodInfo objects - nie wywołuj GetType() w pętli
✅ W 2026 rozważ Source Generators zamiast Reflection!
// Problem: Reflection w runtime
public class JsonSerializer
{
public string Serialize(object obj)
{
Type type = obj.GetType(); // Reflection
var properties = type.GetProperties(); // Reflection
var json = "{";
foreach (var prop in properties)
{
object value = prop.GetValue(obj); // Reflection - WOLNE!
json += $"\"{prop.Name}\": \"{value}\",";
}
json += "}";
return json;
}
}
// Każde serialize() wywołuje Reflection - wolne! 😱
Source Generators - compile-time code generation!
🎉 C# 9+ - Source Generators
Source Generators = generują kod w COMPILE-TIME, nie runtime!
Zero reflection overhead, pełna compile-time safety! ✨
// Source Generator generuje kod w compile-time
// Zamiast Reflection w runtime, masz wygenerowany kod!
// Twój kod:
[AutoSerialize]
public partial class Person
{
public string Name { get; set; }
public int Age { get; set; }
}
// Source Generator generuje (w compile-time):
public partial class Person
{
public string ToJson()
{
return $"{{\"Name\":\"{Name}\",\"Age\":{Age}}}";
}
}
// Użycie - zero Reflection!
var person = new Person { Name = "Jan", Age = 30 };
string json = person.ToJson(); // Wygenerowana metoda - szybka! ✨
// Nie ma Reflection overhead!
Source Generators - przykłady z .NET
// Przykład 1: System.Text.Json Source Generator
// Zamiast Reflection - generuje serialization code!
[JsonSerializable(typeof(Person))]
public partial class PersonJsonContext : JsonSerializerContext { }
// .NET generuje kod serialization w compile-time
var person = new Person { Name = "Jan", Age = 30 };
string json = JsonSerializer.Serialize(person, PersonJsonContext.Default.Person);
// Zero Reflection - wszystko wygenerowane! ✨
// Przykład 2: Regex Source Generator
// Zamiast Reflection - generuje regex code!
public partial class RegexHelpers
{
[GeneratedRegex(@"\d{3}-\d{3}-\d{4}")]
public static partial Regex PhoneNumber();
}
// .NET generuje zoptymalizowany regex code w compile-time
bool isValid = RegexHelpers.PhoneNumber().IsMatch("123-456-7890");
// Zero overhead! ✨
// Przykład 3: Logging Source Generator
public partial class Logger
{
[LoggerMessage(EventId = 0, Level = LogLevel.Information, Message = "User {name} logged in")]
public static partial void LogUserLogin(ILogger logger, string name);
}
// .NET generuje logging code - bez boxing, bez allocations!
Logger.LogUserLogin(_logger, "Jan"); // Super fast! ✨
Reflection vs Source Generators - porównanie
Feature
Reflection (Runtime)
Source Generators (Compile-time)
Kiedy działa
Runtime
Compile-time
Performance
Wolne (10-100x)
Szybkie (jak ręczny kod)
Type safety
Runtime errors
Compile-time errors
Debugging
Trudne
Możesz debugować wygenerowany kod
AOT compatible
❌ Problemy z trimming
✅ Pełne wsparcie
Use case
Dynamic scenarios, tools
Known types w compile-time
💡 Kiedy używać czego?
Source Generators (preferowane w 2026!)
✅ Serialization (JSON, XML, etc.)
✅ Mapping (AutoMapper-like)
✅ Validation
✅ Dependency injection registration
✅ ORM queries (EF-like)
Reflection (tylko gdy musisz)
✅ Plugin systems
✅ Development tools
✅ Truly dynamic scenarios
✅ Working z unknown types
Podsumowanie
Attributes i Reflection - metaprogramowanie w C#!
✅ Attributes - metadane o kodzie, deklaratywne programming
✅ [Obsolete] - deprecation warnings i errors
✅ [Conditional] - conditional compilation, zero overhead
✅ Caller info attributes - CallerMemberName, CallerFilePath, CallerLineNumber
✅ Custom attributes - własne attributes, AttributeUsage, AllowMultiple
✅ ⚠️ Performance - Reflection jest wolne, cachuj objects
✅ 🔥 Source Generators - compile-time alternative, zero overhead!
✅ Reflection vs Generators - kiedy czego używać
W kolejnym wpisie poznasz tzw. partial types - klasy, metody i właściwości!
Zadanie dla Ciebie 🎯
Stwórz prosty validation framework używając attributes i reflection:
// Custom validation attributes
[AttributeUsage(AttributeTargets.Property)]
public class RequiredAttribute : ValidationAttribute { }
[AttributeUsage(AttributeTargets.Property)]
public class MinLengthAttribute : ValidationAttribute
{
public int Length { get; }
public MinLengthAttribute(int length) { Length = length; }
}
[AttributeUsage(AttributeTargets.Property)]
public class RangeAttribute : ValidationAttribute
{
public int Min { get; }
public int Max { get; }
public RangeAttribute(int min, int max) { Min = min; Max = max; }
}
// Validator class
public static class Validator
{
public static ValidationResult Validate(object obj)
{
// Use Reflection to:
// 1. Get all properties
// 2. Check for validation attributes
// 3. Validate values
// 4. Return ValidationResult with errors
}
}
// Model
public class User
{
[Required(ErrorMessage = "Name is required")]
[MinLength(3, ErrorMessage = "Name must be at least 3 characters")]
public string Name { get; set; }
[Range(18, 100, ErrorMessage = "Age must be between 18 and 100")]
public int Age { get; set; }
}
// Użycie
var user = new User { Name = "Jo", Age = 17 };
var result = Validator.Validate(user);
if (!result.IsValid)
{
foreach (var error in result.Errors)
{
Console.WriteLine(error);
}
}
🎯 BONUS: Dependency Injection Container z Reflection