Skip to content

romanesco0728/R3Events

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

229 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

R3Events

CI NuGet Downloads

A C# source generator that automatically bridges .NET events and R3 reactive programming — annotate a static partial class with [R3Event<T>] and get AsObservable() extension methods for every public event on T with zero boilerplate.

NuGet: R3Events

Install-Package R3Events

Introduction

R3Events automatically generates AsObservable() extension methods for all public events on a target type, allowing you to seamlessly convert .NET events into R3 observables.

For example, suppose you have a class with events:

using System.ComponentModel;

class MyControl
{
    public event EventHandler? Click;
    public event CancelEventHandler? BeforeClose;
}

Simply create a static partial class with the R3Event attribute:

using R3Events;

[R3Event(typeof(MyControl))]
static partial class MyControlExtensions
{
}

Or when using C# 11 or later, you can use the generic attribute syntax:

using R3Events;

[R3Event<MyControl>]
static partial class MyControlExtensions
{
}

The generator will automatically create extension methods:

// <auto-generated />
static partial class MyControlExtensions
{
    /// <summary>
    /// Returns an Observable for <c>Click</c>.
    /// </summary>
    public static Observable<Unit> ClickAsObservable(this MyControl instance, CancellationToken cancellationToken = default)
    {
        var rawObservable = Observable.FromEventHandler(
            h => instance.Click += h,
            h => instance.Click -= h,
            cancellationToken
        );
        return rawObservable.AsUnitObservable();
    }

    /// <summary>
    /// Returns an Observable for <c>BeforeClose</c>.
    /// </summary>
    public static Observable<CancelEventArgs> BeforeCloseAsObservable(this MyControl instance, CancellationToken cancellationToken = default)
    {
        var rawObservable = Observable.FromEvent<CancelEventHandler, (object?, CancelEventArgs Args)>(
            h => new CancelEventHandler((s, e) => h((s, e))),
            h => instance.BeforeClose += h,
            h => instance.BeforeClose -= h,
            cancellationToken
        );
        return rawObservable.Select(ep => ep.Args);
    }
}

Now you can use these events as observables:

var control = new MyControl();

// Subscribe to Click event as Observable
control.ClickAsObservable()
    .Subscribe(_ => Console.WriteLine("Clicked!"));

// Subscribe to BeforeClose event with filtering
control.BeforeCloseAsObservable()
    .Where(args => !args.Cancel)
    .Subscribe(args => Console.WriteLine("Closing..."));

Table of Contents

R3EventAttribute

When referencing the R3Events package, it generates an internal R3EventAttribute:

namespace R3Events
{
    [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
    internal sealed class R3EventAttribute : Attribute
    {
        public R3EventAttribute(Type type) { ... }
        public Type Type { get; }
    }
}

When using C# 11 or later, the generator additionally creates a generic variant:

namespace R3Events
{
    [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
    internal sealed class R3EventAttribute<T> : Attribute
    {
    }
}

You can attach this attribute to a static partial class. The generator will scan the specified target type for all public, non-static events and generate corresponding AsObservable() extension methods.

// C# 10 or earlier
[R3Event(typeof(Button))]
static partial class ButtonExtensions { }

// C# 11 or later
[R3Event<Button>]
static partial class ButtonExtensions { }

The attributed class must be:

  • static
  • partial
  • Not nested within another type
  • Not generic

If any of these requirements are not met, the generator will emit a diagnostic error.

Generated Extension Methods

The generator creates extension methods for all public, non-static events on the target type. The method naming convention is {EventName}AsObservable.

EventHandler support

For System.EventHandler (non-generic), the generated observable uses R3.Unit:

public event EventHandler? Click;

// Generates:
public static Observable<Unit> ClickAsObservable(
    this TargetType instance,
    CancellationToken cancellationToken = default)

Generic EventHandler support

For generic event handlers like EventHandler<T>, the generated observable uses the event args type:

public event EventHandler<MouseEventArgs>? MouseMove;

// Generates:
public static Observable<MouseEventArgs> MouseMoveAsObservable(
    this TargetType instance,
    CancellationToken cancellationToken = default)

Custom delegate support

For custom delegates, the generator examines the delegate signature and uses the last parameter as the observable element type:

public delegate void CustomEventHandler(object sender, CustomEventArgs e);
public event CustomEventHandler? CustomEvent;

// Generates:
public static Observable<CustomEventArgs> CustomEventAsObservable(
    this TargetType instance,
    CancellationToken cancellationToken = default)

Diagnostics

R3I001 — Prefer generic R3EventAttribute<T>

Severity: Info
Applies to: C# 11 or later

When C# 11 or later is in use, the generator also emits the generic R3EventAttribute<T>. Using the non-generic [R3Event(typeof(T))] form while a generic alternative is available triggers info diagnostic R3I001 at the attribute site:

R3I001  Type 'MyNamespace.MyClassExtensions' uses R3EventAttribute(typeof(T)).
        Consider using R3EventAttribute<T> instead, which is available in C# 11 and later.

Quick Fix

A code fix (quick action) is available to migrate automatically. Applying it replaces:

// Before
[R3Event(typeof(MyClass))]
internal static partial class MyClassExtensions { }

// After (applied automatically by the code fix)
[R3Event<MyClass>]
internal static partial class MyClassExtensions { }

Any namespace qualification is preserved:

// R3Events.R3Event(typeof(T)) → R3Events.R3Event<T>
[R3Events.R3Event(typeof(MyClass))]
internal static partial class MyClassExtensions { }
// becomes:
[R3Events.R3Event<MyClass>]
internal static partial class MyClassExtensions { }

Note: R3I001 is only emitted when the project's C# language version is 11 or later. For C# 8-10, the non-generic [R3Event(typeof(T))] form is the only option and no info diagnostic is produced.

Requirements

  • .NET Standard 2.0 or later
  • R3 package
  • C# 8.0 or later (generated code includes nullable reference type syntax such as ?)
  • C# 11 or later (only required for generic attribute syntax R3Event<T>)
Install-Package R3
Install-Package R3Events

Important: While R3Events does not have a package dependency on R3, the generated code calls R3 APIs (Observable.FromEvent, Observable.FromEventHandler, etc.). Therefore, you must install the R3 package in your project for the generated code to compile successfully.

License

This library is under the MIT License.

About

C# source generator that converts .NET events into R3 observables via [R3Event<T>] — zero boilerplate.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages