Codectionary / Developer documentation / C#

Events

An event is a special kind of delegate field that provides a publish-subscribe pattern: the declaring class can raise (invoke) the event, but external code can only subscribe (+=) or unsubscribe (-=) - it cannot invoke the event directly or overwrite existing subscribers. This makes events the standard, safe way to notify other parts of a program that something has happened, like a button being clicked or a value changing.

Syntax

public event EventHandler EventName;

Examples

Basic Event with EventHandler

Declaring, subscribing to, and raising a simple event.

public class Button
{
    public event EventHandler? Clicked;

    public void Click()
    {
        Console.WriteLine("Button was clicked");
        Clicked?.Invoke(this, EventArgs.Empty);  // raise the event, if anyone is subscribed
    }
}

Button button = new Button();
button.Clicked += (sender, args) => Console.WriteLine("Handler 1: click received");
button.Clicked += (sender, args) => Console.WriteLine("Handler 2: click received");

button.Click();

Custom Event Data

Defining custom EventArgs to pass extra data along with the event.

public class OrderPlacedEventArgs : EventArgs
{
    public string OrderId { get; }
    public double Total { get; }
    public OrderPlacedEventArgs(string orderId, double total)
    {
        OrderId = orderId;
        Total = total;
    }
}

public class OrderProcessor
{
    public event EventHandler<OrderPlacedEventArgs>? OrderPlaced;

    public void PlaceOrder(string id, double total)
    {
        OrderPlaced?.Invoke(this, new OrderPlacedEventArgs(id, total));
    }
}

OrderProcessor processor = new OrderProcessor();
processor.OrderPlaced += (sender, e) =>
    Console.WriteLine($"Order {e.OrderId} placed for ${e.Total}");

processor.PlaceOrder("ORD-1", 49.99);

Unsubscribing from an Event

Using -= to remove a handler, important for avoiding memory leaks with long-lived publishers.

public class Timer
{
    public event Action? Tick;
    public void Fire() => Tick?.Invoke();
}

Timer timer = new Timer();
void OnTick() => Console.WriteLine("Tick!");

timer.Tick += OnTick;
timer.Fire();  // Tick!

timer.Tick -= OnTick;
timer.Fire();  // nothing printed - handler was removed

Best practices

  • Always use the null-conditional operator (Clicked?.Invoke(...)) when raising an event, since it has no effect (and won't throw) if nobody has subscribed
  • Follow the standard EventHandler / EventHandler<T> pattern rather than a raw Action or Func field, so your events feel familiar to other C# developers
  • Unsubscribe from events (-=) when a subscriber is no longer needed, especially for long-lived publishers - forgetting to do so is a common source of memory leaks
  • Prefer events over a public delegate field for notifications - events restrict outside code to only subscribing/unsubscribing, not raising or overwriting the whole invocation list

At a glance

Purpose
Applications on the .NET platform
File extension
.cs
Runs in
.NET runtime
Usually used with
.NET SDK and libraries

Specifications & further reading

Related C# documentation

Delegates
A delegate is a type-safe reference to a method - essentially a variable that can hold a method and be invoked like one. Delegates enable passing behavior around as data, forming the foundation for events, callbacks, and LINQ. C# provides built-in generic delegate types, Func<> and Action<>, that cover the vast majority of use cases without needing to declare a custom delegate type.
Extension Methods
Extension methods let you add new methods to an existing type - including types you don't own, like built-in .NET types or third-party classes - without modifying its source code or creating a subclass. They're defined as static methods in a static class, with the first parameter prefixed with this to indicate which type they extend. LINQ's Where(), Select(), and friends are themselves implemented as extension methods on IEnumerable<T>.
Nullable Reference Types & Null-Coalescing
Since C# 8, nullable reference types let the compiler track and warn about potential null reference issues at compile time, even for reference types like string and custom classes (which were always implicitly nullable before). A type like string means 'never null' under this feature, while string? explicitly allows null. The null-coalescing operator (??) and null-conditional operator (?.) provide concise syntax for safely working with potentially-null values.
Pattern Matching
Pattern matching, expanded significantly since C# 7, lets you test a value against a shape or condition using is, switch expressions, and more, often extracting matched data in the same step. It covers type patterns (checking and casting in one step), property patterns (matching on an object's properties), and relational patterns (matching numeric ranges), making conditional logic more expressive and less error-prone than manual casting.