Events

From SocialStack
Revision as of 15:40, 17 November 2022 by Luke (talk | contribs) (→‎Dispatching an event)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)

Events are a very commonly used feature of SocialStack API's which allow modules to extend each other without needing to change the code of the modules. They are structured such that they are always autocomplete friendly and follow the dispatch/ listener naming scheme standard.

Defining Events

There is a global static partial class called Events which is extended with your custom event handlers. Typically, such an extension is defined like this:

namespace Api.Eventing;

public partial class Events
{
    // EventGroup<X> defines e.g. AfterCreate, BeforeCreate and similar main events.
    public static EventGroup<Thing> Thing;
}

When you use socialstack generate to create an API module, it defines the above for you in the file called Events.cs.

Firstly, you don't have to use EventGroup<X> - you can use either anything that inherits EventGroup<X>, or an EventHandler<X> which is for a specific event rather than a set of them. X is both the return type of the event, and the type of its first argument (after Context).

It is recommended that, if you want to add a custom event to a specific entity, edit Events.cs and create a little partial event class like so:

public partial class CustomThingEventGroup : EventGroup<Thing>{
    
    // Add custom event handlers in here, like this one.
    // public EventHandler<Thing> AfterSomething;

    // You can have up to 3 custom values passed to your event handler.
    // public EventHandler<Thing, string, int> BeforeSomethingElse;

}

Note that the first custom value is also the return type of the event. For example, if AfterSomething is dispatched, it will return a Thing (typically just the same object it was given, but can be a new instance if the event needs to do so).

Then, edit your Events extension to change it to your newly defined event group class:

namespace Api.Eventing;

public partial class Events
{
    public static CustomThingEventGroup Thing;
}

And that's it - from there, you can use Events.Thing.AfterSomething in other classes. Note that the actual objects are instanced for you, so you don't need to new CustomThingEventGroup() or similar anywhere.

Dispatching an event

To trigger event handlers, use Dispatch. Using the event handler defined above:

// Dispatch the handler, passing the context plus anything that the event requires (up to 3 args, but most commonly just 1).
await Events.Thing.AfterSomething.Dispatch(context, aThing);

Handling an event

To do something when an event occurs, you need to add a handler/ listener to the event. That is via AddEventListener:


// Dispatch the handler, passing the context plus anything that the event requires (up to 3 args, but most commonly just 1).
await Events.Thing.AfterSomething.AddEventListener(async (Context context, Thing thing) => {
    // Some event handlers return null to indicate that the event should halt.
    // It's therefore good practice to null check:
    if(thing == null)
    {
        return null;
    }
    
    // await some stuff
    return thing;
});

// If your event handler is purely synchronous, just return a ValueTask instead:
await Events.Thing.AfterSomething.AddEventListener((Context context, Thing thing) => {
    // Some event handlers return null to indicate that the event should halt.
    // It's therefore good practice to null check:
    if(thing == null)
    {
        return new ValueTask<Thing>(null);
    }
    
    // do some things

    return new ValueTask<Thing>(thing);
});

AddEventListener accepts a priority argument which defaults to 10. The lower the number, the earlier your handler will occur. Use this to define handler order when order is necessary.

Performance

Events are defined as a ValueTask as they almost always do something that requires no waiting at all. Because of the way how ValueTask avoids heap allocations unless waiting for a Task is actually necessary, the event execution system has almost no overhead on release builds. AddEventListener resizes a sorted array and dispatch simply iterates over the array. In summary, event handlers are incredibly cheap - you can easily run hundreds of millions of dispatches per second and it is not an issue, unless of course an actual event listener is quite heavy. The flexibility for other developers gained far outweighs the runtime performance cost of dispatching and handling events.