C#
events
extension methods
programming
software development

Raising C events with an extension method - is it bad?

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

Introduction

In C#, events can only be raised from within the class that declares them. This is a language restriction — external code can only use += and -=. A common workaround is to create an extension method that invokes the event's underlying delegate in a thread-safe way. This pattern is not inherently bad; it simplifies the null-check-and-invoke boilerplate and is widely used in production code. However, it only works when the event is backed by a simple delegate field, and it requires passing the delegate explicitly since extension methods cannot access private members.

Standard Event Raising Pattern

csharp
1public class Button
2{
3    public event EventHandler Click;
4
5    // Traditional thread-safe raise pattern
6    protected virtual void OnClick(EventArgs e)
7    {
8        // Copy to local variable to avoid race condition
9        EventHandler handler = Click;
10        handler?.Invoke(this, e);
11    }
12
13    public void SimulateClick()
14    {
15        OnClick(EventArgs.Empty);
16    }
17}

The On[EventName] pattern is the standard C# convention. You copy the delegate to a local variable (or use ?.Invoke) to prevent a NullReferenceException if the last subscriber unsubscribes between the null check and the invocation.

Extension Method for Raising Events

csharp
1public static class EventExtensions
2{
3    // Generic extension method for EventHandler
4    public static void Raise(this EventHandler handler, object sender, EventArgs e)
5    {
6        handler?.Invoke(sender, e);
7    }
8
9    // Generic extension method for EventHandler<T>
10    public static void Raise<T>(this EventHandler<T> handler, object sender, T e)
11        where T : EventArgs
12    {
13        handler?.Invoke(sender, e);
14    }
15}
16
17// Usage inside the declaring class
18public class Button
19{
20    public event EventHandler Click;
21
22    protected virtual void OnClick()
23    {
24        // Extension method handles null check
25        Click.Raise(this, EventArgs.Empty);
26    }
27}

The extension method encapsulates the null-conditional invocation pattern. The key insight is that Click.Raise(...) works because C# passes the current value of the Click delegate field as the this parameter — creating the same local copy that the manual pattern requires.

Why This Works (Thread Safety)

csharp
1// These two are equivalent:
2
3// Manual pattern
4EventHandler handler = Click;
5handler?.Invoke(this, EventArgs.Empty);
6
7// Extension method pattern
8Click.Raise(this, EventArgs.Empty);
9// Compiler translates this to:
10// EventExtensions.Raise(Click, this, EventArgs.Empty);
11// The value of Click is evaluated once and passed as a parameter

When the compiler evaluates Click.Raise(...), it reads the Click field once and passes that snapshot to the extension method. If another thread sets Click to null after this point, the extension method still holds the original reference. This provides the same thread safety as the manual local-variable copy.

Custom EventArgs with Extension Methods

csharp
1public class DataReceivedEventArgs : EventArgs
2{
3    public string Data { get; }
4    public DataReceivedEventArgs(string data) => Data = data;
5}
6
7public static class EventExtensions
8{
9    public static void Raise<T>(this EventHandler<T> handler, object sender, T e)
10        where T : EventArgs
11    {
12        handler?.Invoke(sender, e);
13    }
14
15    // Convenience overload that creates EventArgs
16    public static void Raise(this EventHandler handler, object sender)
17    {
18        handler?.Invoke(sender, EventArgs.Empty);
19    }
20}
21
22public class DataReceiver
23{
24    public event EventHandler<DataReceivedEventArgs> DataReceived;
25
26    public void OnDataReceived(string data)
27    {
28        DataReceived.Raise(this, new DataReceivedEventArgs(data));
29    }
30}

When Extension Methods Do NOT Work

csharp
1// Custom event accessors — extension method cannot access the backing delegate
2public class CustomButton
3{
4    private EventHandler _click;
5
6    public event EventHandler Click
7    {
8        add { _click += value; }
9        remove { _click -= value; }
10    }
11
12    public void SimulateClick()
13    {
14        // Click.Raise(...) — COMPILE ERROR
15        // "Click" here is the event, not the delegate field
16        // Must use the backing field directly:
17        _click?.Invoke(this, EventArgs.Empty);
18    }
19}
20
21// Interface events — no backing field accessible
22public interface INotifier
23{
24    event EventHandler Notified;
25}

When an event uses custom add/remove accessors, the compiler does not generate a backing field with the same name. The extension method pattern only works with field-like events (the default).

Arguments For and Against

csharp
1// FOR: Cleaner syntax, less boilerplate
2// Before
3protected virtual void OnClick()
4{
5    EventHandler handler = Click;
6    if (handler != null)
7        handler(this, EventArgs.Empty);
8}
9
10// After (with extension method)
11protected virtual void OnClick()
12{
13    Click.Raise(this, EventArgs.Empty);
14}
15
16// AGAINST: C# 6+ null-conditional operator makes it nearly as concise
17protected virtual void OnClick()
18{
19    Click?.Invoke(this, EventArgs.Empty);  // Built-in, no extension needed
20}

Since C# 6 introduced the ?. operator, the primary advantage of the extension method (avoiding the null check) is largely eliminated. Click?.Invoke(this, e) is one line and requires no helper method.

Common Pitfalls

  • Assuming thread safety without understanding why: The extension method is thread-safe because the delegate value is copied when passed as a parameter. If you refactor to pass the event by reference or evaluate it lazily, you lose this guarantee. Understand the mechanism, not just the pattern.
  • Using extension methods with custom event accessors: Events with explicit add/remove do not have a compiler-generated backing field. Calling an extension method on the event name in that case results in a compile error — you must invoke the backing delegate directly.
  • Forgetting that C# 6+ has ?.Invoke(): The null-conditional operator ?. achieves the same null-safe invocation in a single expression. In modern C# codebases, the extension method approach adds indirection without significant benefit.
  • Raising events from outside the declaring class: Extension methods do not bypass C#'s event access rules. You cannot call someObject.Click.Raise(...) from external code — the compiler still restricts access to += and -= only. The extension method is for use inside the class that owns the event.
  • Not making the raise method virtual: When using the traditional OnEventName pattern, marking it protected virtual allows derived classes to override the behavior (e.g., suppress or modify events). Extension methods are static and cannot be overridden, so subclasses lose this customization point.

Summary

  • Extension methods for raising events encapsulate the null-check-and-invoke pattern and are thread-safe
  • They work only with field-like events (no custom add/remove accessors)
  • Since C# 6, Click?.Invoke(this, e) achieves the same result without a helper method
  • The traditional protected virtual OnEventName() pattern remains preferred when subclasses need to override event raising
  • Extension methods cannot be used to raise events from outside the declaring class — C# enforces this restriction regardless

Related reading
Course
Intermediate
27 lessons
14 hours
OOD Fundamentals

Master object-oriented design from first principles, SOLID, design patterns, and classic interview problems with hands-on coding.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

All Rights Reserved.