Using c# eventargs for Custom Event Data
Learn how to use c# eventargs to pass data in events, create custom event argument classes, and follow best practices for .NET event design.
When you declare an event in C#, the handler signature typically includes a sender object and an EventArgs-derived parameter. The c# eventargs base class is the contract that carries event-specific data. Understanding how to use it correctly determines how clearly your events communicate state without leaking implementation details.
The Standard EventArgs and Its Limitations
The EventArgs class itself is empty. It exists as a marker type so that all event handlers share a common signature. The framework's EventHandler delegate uses EventArgs directly, which means events that need no data can pass EventArgs.Empty—a static instance that avoids allocating a new object each time.
public event EventHandler? SomethingHappened;
Raising this event with no data looks like this:
SomethingHappened?.Invoke(this, EventArgs.Empty);
The limitation becomes obvious when you need to pass meaningful information. A temperature sensor, for example, needs to report the current reading. The standard EventArgs cannot carry that value, so you must create a custom class derived from EventArgs.
Creating a Custom EventArgs Class
A custom event argument class is a simple class that inherits from EventArgs and exposes properties for the data you want to send. The naming convention is to append EventArgs to the class name.
public class TemperatureChangedEventArgs : EventArgs { public double TemperatureCelsius { get; } public TemperatureChangedEventArgs(double temperatureCelsius) { TemperatureCelsius = temperatureCelsius; } }
The constructor takes the value and stores it in a read-only property. This makes the event data immutable after it is created, which is important because the event may be handled by multiple subscribers. If one subscriber mutates the data, it could affect others. Immutability prevents that class of bugs.
Declaring and Raising an Event with Custom Data
To use the custom event args, declare the event with the generic EventHandler<TEventArgs> delegate:
public event EventHandler<TemperatureChangedEventArgs>? TemperatureChanged;
When raising the event, create an instance of the custom args and pass it to the handler:
private void OnTemperatureChanged(double temperature) { TemperatureChanged?.Invoke(this, new TemperatureChangedEventArgs(temperature)); }
Subscribers can then read the property from the e parameter:
sensor.TemperatureChanged += (sender, e) => { Console.WriteLine($"New temperature: {e.TemperatureCelsius}°C"); };
The sender parameter is still available, but in many cases the event args are the more important part because they carry the actual data.
Designing Event Args for Maintainability
A well-designed event args class should be small and focused. It should contain only the data that handlers need to react to the event. If you need to pass many fields, consider grouping them into a separate immutable record or class rather than creating a wide event args class.
Use read-only properties or init accessors to prevent modification after construction. In modern C#, you can use a record:
public record TemperatureChangedEventArgs(double TemperatureCelsius) : EventArgs;
This gives you value-based equality and a concise declaration, but it still derives from EventArgs so it works with the standard event pattern.
Avoid putting logic inside event args. They are data containers, not services. If you need to compute a derived value, expose a property that does the calculation, but keep it stateless.
Performance and Allocation Considerations
Every time you raise an event with custom args, you allocate a new object. For most applications this is negligible, but for high-frequency events—such as a timer that fires every millisecond—allocation pressure can become measurable.
If an event carries no data, always use EventArgs.Empty instead of creating a new instance. For events that carry data, consider whether the data can be reused. If the event args are immutable and the values are the same, you could cache a single instance. However, this is rarely necessary unless profiling shows a problem.
Another option is to use a struct for event data, but the EventHandler<TEventArgs> delegate requires TEventArgs to be a reference type because it has a class constraint. You would need a custom delegate to use a struct, which breaks the standard event pattern. In practice, the allocation cost of a small class is acceptable for most scenarios.
Alternatives to the Standard EventArgs Pattern
The standard event pattern is not the only way to notify subscribers. You can use Action<T> delegates or custom delegate types that pass data directly. For example:
public event Action<double>? TemperatureChanged;
This is simpler and avoids the EventArgs boilerplate. However, it loses the common sender parameter and the convention that makes events uniform across .NET. The EventHandler<TEventArgs> pattern is preferred in libraries and frameworks because it is recognizable and supports multiple subscribers without extra plumbing.
If you are designing a public API, stick with the standard pattern. If you are writing internal code with a single subscriber, a simple Action<T> may be sufficient. The tradeoff is consistency versus brevity.
When to Break the EventArgs Convention
There are rare cases where deriving from EventArgs adds no value. For example, if you are using a functional reactive library or a message bus, events might not be the right abstraction. But within the classic .NET event model, EventArgs is the expected base class. Some code analyzers and frameworks enforce this convention, so deviating from it can cause compatibility issues.
The EventArgs base class also provides a common Empty static field and a ToString override that returns the type name. These are minor conveniences, but they are part of the contract that other developers expect.
In short, use custom event args when you need to pass data, keep them immutable, and follow the naming convention. The pattern is simple, but it is the foundation of event-driven design in .NET.