Code that reads, builds or writes other code. C# can do it at run time with reflection and expression trees, or at build time with source generators. Build time wins when you can choose.
The page moves from run time to build time. Reflection, attributes, dynamic and expression trees inspect and create code while the program runs. Source generators, interceptors and module initializers act at compile time or load time. Partial members, the field keyword and extension members come from C# 13 and 14. They help generated code and hand-written code fit together.
Every type carries metadata. System.Reflection reads it at run time. From a Type you can list properties, methods, fields and attributes. You can read and write values, call methods, and create instances, all by name.
MethodInfo.Invoke is many times slower than a direct call, and it boxes arguments. Turn hot lookups into delegates with CreateDelegate or compiled expression trees.using System.Reflection;
public sealed class Widget
{
public string Name { get; set; } = "";
public int Size { get; private set; }
/// <summary>Grows the widget.</summary>
/// <param name="by">Amount to add.</param>
/// <returns>The new size.</returns>
/// <example>new Widget().Grow(3) returns 3.</example>
public int Grow(int by) => Size += by;
private string Secret() => "hidden";
}
public static class Reflect
{
/// <summary>Lists public instance property names, sorted.</summary>
/// <param name="t">The type to inspect.</param>
/// <returns>Property names in order.</returns>
/// <example>Reflect.PropertyNames(typeof(Widget)) returns ["Name", "Size"].</example>
public static List<string> PropertyNames(Type t)
=> t.GetProperties(BindingFlags.Public | BindingFlags.Instance)
.Select(p => p.Name)
.Order()
.ToList();
/// <summary>Calls Grow by name through MethodInfo.Invoke.</summary>
/// <param name="w">The target widget.</param>
/// <param name="by">Amount to add.</param>
/// <returns>The new size.</returns>
/// <example>Reflect.InvokeGrow(new Widget(), 4) returns 4.</example>
public static int InvokeGrow(Widget w, int by)
{
MethodInfo m = typeof(Widget).GetMethod(nameof(Widget.Grow))!;
// Invoke takes object?[] and returns object?, so by is boxed and the result unboxed.
return (int)m.Invoke(w, [by])!;
}
/// <summary>Calls a private method, which reflection allows.</summary>
/// <param name="w">The target widget.</param>
/// <returns>The private method's result.</returns>
/// <example>Reflect.CallPrivate(new Widget()) returns "hidden".</example>
public static string CallPrivate(Widget w)
=> (string)typeof(Widget)
.GetMethod("Secret", BindingFlags.NonPublic | BindingFlags.Instance)!
.Invoke(w, null)!;
/// <summary>Sets any property by name.</summary>
/// <param name="target">The object.</param>
/// <param name="property">The property name.</param>
/// <param name="value">The new value.</param>
/// <returns>The value read back through reflection.</returns>
/// <example>Reflect.SetByName(new Widget(), "Name", "gear") returns "gear".</example>
public static object? SetByName(object target, string property, object? value)
{
PropertyInfo p = target.GetType().GetProperty(property)
?? throw new ArgumentException($"No property {property}.", nameof(property));
p.SetValue(target, value);
return p.GetValue(target);
}
/// <summary>Turns Grow into a typed delegate once, for fast repeated calls.</summary>
/// <returns>An open-instance delegate: the first argument is the target.</returns>
/// <example>Reflect.FastGrow()(new Widget(), 5) returns 5.</example>
public static Func<Widget, int, int> FastGrow()
=> typeof(Widget).GetMethod(nameof(Widget.Grow))!
.CreateDelegate<Func<Widget, int, int>>();
/// <summary>Creates an instance from a Type with a parameterless constructor.</summary>
/// <param name="t">The type.</param>
/// <returns>The new object's type name.</returns>
/// <example>Reflect.CreateName(typeof(Widget)) returns "Widget".</example>
public static string CreateName(Type t) => Activator.CreateInstance(t)!.GetType().Name;
}
T.Check("props", Reflect.PropertyNames(typeof(Widget)), ["Name", "Size"]);
T.Check("invoke", Reflect.InvokeGrow(new Widget(), 4), 4);
T.Check("private", Reflect.CallPrivate(new Widget()), "hidden");
T.Check("set by name", Reflect.SetByName(new Widget(), "Name", "gear"), "gear");
T.Check("fast grow", Reflect.FastGrow()(new Widget(), 5), 5);
nameof where you can.TargetInvocationException.GetProperties order is not guaranteed. Sort if order matters.[DynamicallyAccessedMembers].CreateDelegate or a compiled expression.BindingFlags.NonPublic. Access modifiers guide the compiler, not the runtime. Do not rely on it outside tests and tools.An attribute is a class that derives from Attribute. Placing [Name(args)] on code stores the arguments in metadata. Nothing runs until some code asks for the attribute through reflection, or a source generator reads it at build time. [AttributeUsage] limits where it may go.
Since C# 11, an attribute can be generic: [Validator<SignupValidator>] instead of [Validator(typeof(SignupValidator))]. The compiler then checks constraints on the type argument.
typeof.using System.Reflection;
[AttributeUsage(AttributeTargets.Property)]
public sealed class MaxLengthAttribute(int length) : Attribute
{
public int Length { get; } = length;
}
public interface IValidator
{
bool IsValid(object value);
}
// Non-generic view of the generic attribute, so callers can find it without knowing T.
public interface IValidatorSource
{
IValidator Create();
}
// C# 11 generic attribute. The constraint is checked at the place it is used.
[AttributeUsage(AttributeTargets.Class)]
public sealed class ValidatorAttribute<TValidator> : Attribute, IValidatorSource
where TValidator : IValidator, new()
{
public IValidator Create() => new TValidator();
}
public sealed class SignupValidator : IValidator
{
public bool IsValid(object value) => value is Signup s && s.Email.Contains('@');
}
[Validator<SignupValidator>]
public sealed class Signup
{
[MaxLength(5)] // 5: a short limit so the demo can break it easily
public string User { get; set; } = "";
public string Email { get; set; } = "";
}
public static class Validation
{
/// <summary>Checks MaxLength on each property, then runs the class-level validator.</summary>
/// <param name="obj">The object to validate.</param>
/// <returns>A list of error messages, empty when valid.</returns>
/// <example>Validation.Errors(new Signup { User = "ada", Email = "a@b" }) returns [].</example>
public static List<string> Errors(object obj)
{
List<string> errors = [];
// p is the current property. Invariant: errors covers every earlier property.
foreach (PropertyInfo p in obj.GetType().GetProperties())
{
var max = p.GetCustomAttribute<MaxLengthAttribute>();
if (max is not null && p.GetValue(obj) is string s && s.Length > max.Length)
{
errors.Add($"{p.Name} too long");
}
}
// Find the generic attribute through its non-generic interface.
foreach (IValidatorSource source in obj.GetType().GetCustomAttributes()
.OfType<IValidatorSource>())
{
if (!source.Create().IsValid(obj))
{
errors.Add("invalid");
}
}
return errors;
}
}
T.Check("valid", Validation.Errors(new Signup { User = "ada", Email = "a@b" }), []);
T.Check("too long", Validation.Errors(new Signup { User = "adalovelace", Email = "a@b" }),
["User too long"]);
T.Check("bad email", Validation.Errors(new Signup { User = "ada", Email = "x" }), ["invalid"]);
typeof expressions, or arrays of those.[Validator<T>] inside a generic class is an error.GetCustomAttribute call creates a new attribute instance. Cache the results.typeof(X) arguments had no type checks. [Attr<X>] lets the compiler enforce constraints such as where X : IValidator, new().[CallerMemberName], [CallerFilePath], [CallerLineNumber] on optional parameters. The compiler fills them in at each call site with the caller’s method name, file and line.[CallerArgumentExpression] (C# 10) passes the argument’s source text. See A 07.nameof(x) turns a symbol into its name as a constant string. Renames update it automatically.Logging and tracing helpers use caller info to record where they were called. INotifyPropertyChanged uses [CallerMemberName] so setters need no string. Use nameof anywhere a member name appears in a string: exceptions, attributes, logs.
using System.ComponentModel;
using System.Runtime.CompilerServices;
public static class Trace
{
/// <summary>Reports which member called it and whether a line number was supplied.</summary>
/// <param name="member">Filled in by the compiler.</param>
/// <param name="line">Filled in by the compiler.</param>
/// <returns>"member@line-known".</returns>
/// <example>Trace.Here() called from Run returns "Run@line-known".</example>
public static string Here([CallerMemberName] string member = "",
[CallerLineNumber] int line = 0)
// 0 is the default, so a positive number proves the compiler filled it in.
=> $"{member}@{(line > 0 ? "line-known" : "no-line")}";
/// <summary>Calls Here so the reported member is this method.</summary>
/// <returns>"Run@line-known".</returns>
/// <example>Trace.Run() returns "Run@line-known".</example>
public static string Run() => Here();
}
public sealed class Profile : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
public string Name
{
get;
set => SetField(ref field, value); // C# 14 field keyword, see section 10
} = "";
public int Age
{
get;
set => SetField(ref field, value);
}
// [CallerMemberName] fills in "Name" or "Age" with no string literal.
private void SetField<TValue>(ref TValue field, TValue value,
[CallerMemberName] string? name = null)
{
if (EqualityComparer<TValue>.Default.Equals(field, value))
{
return;
}
field = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
}
public static class Notify
{
/// <summary>Changes properties and records which names were raised.</summary>
/// <returns>The raised property names.</returns>
/// <example>Notify.Changes() returns ["Name", "Age"].</example>
public static List<string> Changes()
{
var p = new Profile();
List<string> raised = [];
p.PropertyChanged += (_, e) => raised.Add(e.PropertyName ?? "?");
p.Name = "Ada";
p.Age = 36; // 36: sample age
p.Age = 36; // same value again: no event
return raised;
}
/// <summary>Shows nameof on a type, a member and a generic type.</summary>
/// <returns>The three names.</returns>
/// <example>Notify.Names() returns ["Profile", "Age", "List"].</example>
public static List<string> Names() =>
[nameof(Profile), nameof(Profile.Age), nameof(List<int>)];
}
T.Check("caller member", Trace.Run(), "Run@line-known");
T.Check("notify", Notify.Changes(), ["Name", "Age"]);
T.Check("nameof", Notify.Names(), ["Profile", "Age", "List"]);
[CallerFilePath] embeds full build paths in the binary. Use PathMap or deterministic builds to hide them.nameof(a.b.c) gives only "c", the last part.[CallerMemberName] work? The compiler sees the attribute on an optional parameter and passes the calling member’s name as a constant at each call site. There is no run-time cost.nameof over a string literal? It is checked by the compiler and updated by rename refactorings. A literal silently goes stale.A dynamic value skips compile-time checking. Every member access, call and operator on it is bound at run time by the Dynamic Language Runtime. The DLR caches each call site, so repeat calls with the same types are fairly quick. ExpandoObject is a bag you add members to at run time. DynamicObject lets you intercept member access yourself.
JsonNode or typed models.using System.Dynamic;
public static class Dyn
{
/// <summary>Adds two values of any types that support +, chosen at run time.</summary>
/// <param name="x">Any value.</param>
/// <returns>x + x, as bound at run time.</returns>
/// <example>Dyn.Twice(2) returns 4, Dyn.Twice("ab") returns "abab".</example>
public static object Twice(dynamic x) => x + x;
/// <summary>Adds members to an ExpandoObject at run time.</summary>
/// <returns>The name plus the member count.</returns>
/// <example>Dyn.Expando() returns "Ada 2".</example>
public static string Expando()
{
dynamic bag = new ExpandoObject();
bag.Name = "Ada";
bag.Age = 36; // 36: sample age
// ExpandoObject is also a dictionary of its members.
var members = (IDictionary<string, object?>)bag;
string name = bag.Name;
return $"{name} {members.Count}";
}
/// <summary>Calls a method that does not exist on string.</summary>
/// <returns>Never returns. Throws RuntimeBinderException.</returns>
/// <example>Dyn.CallMissing() throws.</example>
public static object CallMissing()
{
dynamic s = "text";
return s.NoSuchMethod();
}
/// <summary>Records calls on a DynamicObject.</summary>
/// <returns>The recorded call names.</returns>
/// <example>Dyn.Record() returns ["Open", "Close"].</example>
public static List<string> Record()
{
dynamic r = new CallRecorder();
r.Open(1); // 1: any argument
r.Close();
return ((CallRecorder)r).Calls;
}
}
public sealed class CallRecorder : DynamicObject
{
public List<string> Calls { get; } = [];
// Runs for every method call the compiler could not bind.
public override bool TryInvokeMember(InvokeMemberBinder binder, object?[]? args,
out object? result)
{
Calls.Add(binder.Name);
result = args?.Length ?? 0; // 0 when there are no arguments
return true;
}
}
T.Check("twice int", Dyn.Twice(2), 4);
T.Check("twice string", Dyn.Twice("ab"), "abab");
T.Check("expando", Dyn.Expando(), "Ada 2");
T.Throws<Microsoft.CSharp.RuntimeBinder.RuntimeBinderException>("missing", () => Dyn.CallMissing());
T.Check("recorder", Dyn.Record(), ["Open", "Close"]);
RuntimeBinderExceptions.dynamic becomes dynamic too. Assign to a typed local early.dynamic receivers.dynamic is not supported under Native AOT.dynamic vs object vs var? var is a static type the compiler infers. object is static and needs casts. dynamic defers all binding to run time.dynamic slow? The first call at a site is slow because it binds. Later calls with the same types hit the call-site cache and are much faster, but still slower than static calls.An Expression<Func<...>> is code as data. The compiler turns a lambda into a tree of nodes instead of IL. You can inspect the tree, translate it, as Entity Framework does into SQL, or call Compile() to get a real delegate. You can also build trees by hand with the Expression factory methods.
Reading the figure. Violet is the lambda root. Amber nodes are operators. Blue is the parameter and green are constants. A LINQ provider walks this tree from the top and emits SQL. Compile() walks it and emits IL.
IQueryable receives expression trees so it can translate them.x => x.Name gives you the member name with refactoring support.using System.Linq.Expressions;
public static class Exprs
{
/// <summary>Builds x => x * a + b by hand and compiles it.</summary>
/// <param name="a">The multiplier.</param>
/// <param name="b">The offset.</param>
/// <returns>A compiled delegate.</returns>
/// <example>Exprs.Linear(2, 1)(5) returns 11.</example>
public static Func<int, int> Linear(int a, int b)
{
ParameterExpression x = Expression.Parameter(typeof(int), "x");
// Build bottom-up: (x * a), then (x * a) + b.
BinaryExpression body = Expression.Add(
Expression.Multiply(x, Expression.Constant(a)),
Expression.Constant(b));
return Expression.Lambda<Func<int, int>>(body, x).Compile();
}
/// <summary>Prints a lambda's expression tree.</summary>
/// <param name="e">A lambda the compiler turned into a tree.</param>
/// <returns>The tree as text.</returns>
/// <example>Exprs.Show(x => x * 2 + 1) returns "x => ((x * 2) + 1)".</example>
public static string Show(Expression<Func<int, int>> e) => e.ToString();
/// <summary>Compiles a property getter by name. Faster than PropertyInfo.GetValue.</summary>
/// <param name="property">The property name.</param>
/// <returns>A getter delegate that boxes the value to object.</returns>
/// <example>Exprs.Getter<Widget>("Name")(new Widget { Name = "a" }) returns "a".</example>
public static Func<TObj, object?> Getter<TObj>(string property)
{
ParameterExpression o = Expression.Parameter(typeof(TObj), "o");
// Convert to object so one delegate type fits any property type.
UnaryExpression body = Expression.Convert(Expression.Property(o, property), typeof(object));
return Expression.Lambda<Func<TObj, object?>>(body, o).Compile();
}
/// <summary>Extracts the member name from a selector like x => x.Size.</summary>
/// <param name="selector">A member access lambda.</param>
/// <returns>The member name.</returns>
/// <example>Exprs.MemberName((Widget w) => w.Size) returns "Size".</example>
public static string MemberName<TObj, TProp>(Expression<Func<TObj, TProp>> selector)
=> selector.Body is MemberExpression m
? m.Member.Name
: throw new ArgumentException("Expected a member access.", nameof(selector));
}
T.Check("linear", Exprs.Linear(2, 1)(5), 11);
T.Check("show", Exprs.Show(x => x * 2 + 1), "x => ((x * 2) + 1)");
T.Check("getter", Exprs.Getter<Widget>("Name")(new Widget { Name = "a" }), "a");
T.Check("member", Exprs.MemberName((Widget w) => w.Size), "Size");
Compile() is costly, often a millisecond or more. Compile once and cache the delegate.await, or newer syntax like ?. and collection expressions in many cases.Func<T, bool> vs Expression<Func<T, bool>> in a LINQ query? A Func is compiled code, so filtering runs in memory. An Expression is data, so a provider can translate it to SQL and filter in the database.GetValue pays lookup and boxing costs on every call.A source generator is a compiler plug-in. It runs during the build, reads your code through the Roslyn APIs, and adds new C# files to the compilation. It can only add code. It cannot change what you wrote. The incremental API, IIncrementalGenerator, builds a pipeline of cached steps so the IDE stays fast as you type.
Reading the figure. Blue is code you write. Amber steps are cached pipeline stages. Green is the generated file. Violet is the normal compile, which sees your code and the generated code together.
System.Text.Json, Regex, logging and P/Invoke.ToString, equality, mappers, INotifyPropertyChanged.This block does not compile in the site’s harness. A generator lives in its own netstandard2.0 project that references Microsoft.CodeAnalysis.CSharp, and the app references it as an analyzer.
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
[Generator]
public sealed class AutoToStringGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
// Step 1: add the marker attribute to every compilation that uses the generator.
context.RegisterPostInitializationOutput(ctx => ctx.AddSource(
"AutoToStringAttribute.g.cs",
"[System.AttributeUsage(System.AttributeTargets.Class)]\n" +
"internal sealed class AutoToStringAttribute : System.Attribute { }"));
// Step 2: find partial classes marked [AutoToString], keep only a small model.
var models = context.SyntaxProvider.ForAttributeWithMetadataName(
"AutoToStringAttribute",
predicate: static (node, _) => node is ClassDeclarationSyntax,
transform: static (ctx, _) =>
{
var type = (INamedTypeSymbol)ctx.TargetSymbol;
var props = type.GetMembers().OfType<IPropertySymbol>()
.Select(p => p.Name).ToArray();
return (Ns: type.ContainingNamespace.ToDisplayString(),
Name: type.Name, Props: string.Join(",", props));
});
// Step 3: emit one file per model. Cached models skip this step.
context.RegisterSourceOutput(models, static (spc, m) =>
{
var parts = m.Props.Split(',', StringSplitOptions.RemoveEmptyEntries)
.Select(p => $"{p} = {{{p}}}");
string code = $$"""
namespace {{m.Ns}};
partial class {{m.Name}}
{
public override string ToString()
=> $"{{m.Name}} { {{string.Join(", ", parts)}} }";
}
""";
spc.AddSource($"{m.Name}.g.cs", SourceText.From(code, Encoding.UTF8));
});
}
}
// In the app project:
// [AutoToString]
// public partial class Point { public int X { get; set; } public int Y { get; set; } }
// new Point { X = 1, Y = 2 }.ToString() returns "Point { X = 1, Y = 2 }"
ForAttributeWithMetadataName. It is much faster than scanning every syntax node yourself.ISourceGenerator API is deprecated. Use IIncrementalGenerator.partial types and partial members.An interceptor is a method marked [InterceptsLocation(...)]. It tells the compiler: “at this exact call site, call me instead”. It shipped as a C# 12 preview and is still an opt-in feature. A project must list allowed namespaces in <InterceptorsNamespaces>. Since the .NET 9 SDK the location is an opaque hash from the Roslyn API, not a file path, line and column.
This block does not compile in the site’s harness. It needs the preview opt-in in the project file, and the location data must come from a generator.
// Program.cs (hand-written)
var greeter = new Greeter();
greeter.Hello("Ada"); // the generator intercepts this exact call
public sealed class Greeter
{
public string Hello(string name) => $"Hello {name}";
}
// Generated.g.cs (emitted by a source generator)
namespace System.Runtime.CompilerServices
{
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
file sealed class InterceptsLocationAttribute(int version, string data) : Attribute { }
}
namespace MyApp.Generated
{
static file class Interceptors
{
// version 1 and data come from SemanticModel.GetInterceptableLocation(...)
[System.Runtime.CompilerServices.InterceptsLocation(1, "base64-hash-of-call-site")]
public static string FastHello(this Greeter g, string name) => "Hi " + name;
}
}
// MyApp.csproj
// <InterceptorsNamespaces>$(InterceptorsNamespaces);MyApp.Generated</InterceptorsNamespaces>
this for instance methods.A method marked [ModuleInitializer] runs once when the assembly loads, before any other code in it. It must be static, return void, take no parameters, and be internal or public.
using System.Runtime.CompilerServices;
public static class Registry
{
public static List<string> Codecs { get; } = [];
// Runs once, when this assembly loads, before Tests.Run touches anything.
[ModuleInitializer]
internal static void RegisterDefaults()
{
Codecs.Add("json");
Codecs.Add("csv");
}
/// <summary>Lists the codecs registered at load time.</summary>
/// <returns>The codec names.</returns>
/// <example>Registry.Snapshot() returns ["json", "csv"].</example>
public static List<string> Snapshot() => [.. Codecs];
}
T.Check("module init", Registry.Snapshot(), ["json", "csv"]);
A partial member has a defining declaration with no body and an implementing declaration with the body. The two parts can sit in different files of the same partial type. This is how hand-written and generated code meet. You declare the shape, and a generator writes the body, or the reverse.
partial void OnX(); with no access modifier is optional. If nobody implements it, the call disappears. Since C# 9, a partial method may return a value or have an access modifier, but then it must be implemented.public partial string Name { get; set; }. The implementing part has accessor bodies. [GeneratedRegex] on a partial property uses this.: this(...) or : base(...). The implementing event must have add and remove.Use them when a generator must fill in a member you name: regex, logging, P/Invoke, view-model properties, weak events. Outside generators, splitting a class across files this way is rarely worth it.
// Part 1: the shape. A generator would usually read or write one of these parts.
public partial class Thermostat
{
// Optional hook: removed by the compiler if nobody implements it.
partial void OnChanged(double celsius);
public partial double Celsius { get; set; } // C# 13 partial property
public partial string this[char unit] { get; } // C# 13 partial indexer
public partial Thermostat(double celsius); // C# 14 partial constructor
public partial event Action<double>? Changed; // C# 14 partial event
}
// Part 2: the bodies.
public partial class Thermostat
{
private double _celsius;
private Action<double>? _changed;
public List<double> History { get; } = [];
public partial Thermostat(double celsius) => _celsius = celsius;
public partial double Celsius
{
get => _celsius;
set
{
_celsius = value;
OnChanged(value);
_changed?.Invoke(value);
}
}
public partial string this[char unit] => unit switch
{
// * 9 / 5 + 32 converts Celsius to Fahrenheit.
'F' => $"{_celsius * 9 / 5 + 32}",
// + 273.15 converts Celsius to Kelvin.
'K' => $"{_celsius + 273.15}",
_ => $"{_celsius}",
};
// The implementing event must spell out add and remove.
public partial event Action<double>? Changed
{
add => _changed += value;
remove => _changed -= value;
}
partial void OnChanged(double celsius) => History.Add(celsius);
}
public static class PartialDemo
{
/// <summary>Exercises every partial member of Thermostat.</summary>
/// <returns>Readings in F and K, plus history and event counts.</returns>
/// <example>PartialDemo.Run() returns "77 298.15 history=2 events=2".</example>
public static string Run()
{
var t = new Thermostat(20); // 20 C: a starting room temperature
int events = 0; // 0: no events yet
t.Changed += _ => events++;
t.Celsius = 21; // 21 C: first change
t.Celsius = 25; // 25 C: second change, 77 F
return $"{t['F']} {t['K']} history={t.History.Count} events={events}";
}
}
T.Check("partial", PartialDemo.Run(), "77 298.15 history=2 events=2");
field.partial void method may lack an implementation. Every other partial member needs both parts.partial void method? The compiler removes the call, including the evaluation of its arguments.[GeneratedRegex] wanted properties, and MVVM and interop generators wanted constructors and events.Inside a property accessor, field refers to a backing field the compiler creates. You can write one accessor by hand and leave the other automatic. Before C# 14, adding any logic to a setter meant declaring a private field yourself and writing both accessors.
set => field = value.Trim();.get => field ??= Load();.Profile class in section 3.public sealed class Person
{
// The setter trims and validates. The getter stays automatic.
public string Name
{
get;
set => field = string.IsNullOrWhiteSpace(value)
? throw new ArgumentException("Name is required.", nameof(value))
: value.Trim();
} = "unknown"; // initializer writes the backing field directly, no setter call
// Clamps to 0..150, a generous range for a human age.
public int Age
{
get;
set => field = Math.Clamp(value, 0, 150);
}
public int LoadCount { get; private set; }
// Created on first read, then reused.
public List<string> Tags => field ??= LoadTags();
private List<string> LoadTags()
{
LoadCount++;
return ["new"];
}
}
public static class FieldDemo
{
/// <summary>Exercises the trimming setter, the clamp and the lazy getter.</summary>
/// <returns>A summary of the resulting state.</returns>
/// <example>FieldDemo.Run() returns "Ada 150 tags=1 loads=1".</example>
public static string Run()
{
var p = new Person { Name = " Ada ", Age = 200 }; // 200: above the clamp limit
_ = p.Tags;
_ = p.Tags; // second read reuses the same list
return $"{p.Name} {p.Age} tags={p.Tags.Count} loads={p.LoadCount}";
}
}
T.Check("field", FieldDemo.Run(), "Ada 150 tags=1 loads=1");
T.Check("default name", new Person().Name, "unknown");
T.Throws<ArgumentException>("blank name", () => new Person { Name = " " });
field is shadowed inside accessors. Use @field or this.field to reach it.field ??= is not thread-safe. Use Lazy<T> for shared objects.field replace? The hand-written private backing field you needed as soon as one accessor had logic.field as a variable name? Yes outside accessors. Inside an accessor it is a keyword, so write @field for the old meaning.Extension methods (C# 3) let a static method look like an instance method. C# 14 generalizes them with an extension block inside a static class. The block names the receiver once. Inside it you can declare instance methods, properties, and static members that appear on the extended type itself, such as int.Squared(3). Old this-parameter extension methods still work and can sit beside the new form.
GetX() methods: text.IsBlank, list.IsEmpty.dynamic, or interceptors, there is no run-time magic, no AOT issue, and full IDE support. Often they are all the “metaprogramming” you need.public static class TextExtensions
{
// One receiver, many members. s is the string the member is called on.
extension(string s)
{
/// <summary>True when the text is null, empty or whitespace.</summary>
/// <example>" ".IsBlank returns true.</example>
public bool IsBlank => string.IsNullOrWhiteSpace(s);
/// <summary>Counts words split on spaces.</summary>
/// <example>"to be or".WordCount returns 3.</example>
public int WordCount => s.Split(' ', StringSplitOptions.RemoveEmptyEntries).Length;
/// <summary>Upper-cases and adds an exclamation mark.</summary>
/// <returns>The shouted text.</returns>
/// <example>"hi".Shout() returns "HI!".</example>
public string Shout() => s.ToUpperInvariant() + "!";
}
// Generic receiver: works for any sequence.
extension<TItem>(IEnumerable<TItem> source)
{
/// <summary>True when the sequence has no items.</summary>
/// <example>new List<int>().IsEmpty returns true.</example>
public bool IsEmpty => !source.Any();
}
// No receiver name: only static members, called on the type itself.
extension(int)
{
/// <summary>Squares a number. Called as int.Squared(x).</summary>
/// <param name="x">The number.</param>
/// <returns>x times x.</returns>
/// <example>int.Squared(7) returns 49.</example>
public static int Squared(int x) => x * x;
}
}
public static class ExtensionDemo
{
/// <summary>Uses each kind of extension member once.</summary>
/// <returns>The results joined with spaces.</returns>
/// <example>ExtensionDemo.Run() returns "True 3 HI! True 49".</example>
public static string Run()
{
List<int> none = [];
// 7: any sample number for the static extension.
return $"{" ".IsBlank} {"to be or".WordCount} {"hi".Shout()} "
+ $"{none.IsEmpty} {int.Squared(7)}";
}
}
T.Check("extensions", ExtensionDemo.Run(), "True 3 HI! True 49");
T.Check("not blank", "x".IsBlank, false);
T.Check("not empty", new[] { 1 }.IsEmpty, false);
source.IsEmpty on a lazy query may run part of it.int.Squared. Operators are allowed too.nameof remove magic strings.dynamic defers binding to run time. Expression trees are code as data that you can translate or compile.field (C# 14) join generated and hand-written code with less boilerplate.Next is code that runs at the same time as other code. Concurrency covers locks, the new Lock type, Interlocked and concurrent collections.