Skip to main content

Declarative routing pattern

The first convenient way to receive commands is the [Routes] attribute. Types with [Routes] attributes are parsed at compile-time to receive commands of the type specified in the first argument.

note

Types with [Routes] attribute must be partial. This is because some methods are added automatically.

[Routes]
public partial class FooPresentor
{
// ...
}

Appending [Route] to a method of this class will cause the command to be delivered.

 [Routes]
public partial class FooPresentor
{
[Route]
void On(FooCommand cmd)
{
// Do something ...
}

[Route]
async ValueTask On(BarCommand cmd, CancellationToken cancellation)
{
await DoSomethingAwait();
}
}
  • The [Route] method can also be async.
    • The return value can be any of ValueTask, Task, UniTask, or UnityEngine.Awaitable.
  • Multiple handlers can be defined in one class.
  • Method names are arbitrary. The example on this page uses the name On, but you can use any other name.
    • For example, void Handle(FooCommand, cmd).
  • The second argument may or may not be present, but may take the following.
    • CancellationToken.
      • You may also receive a CancellationToken as the second argument. This will be cancelled if the Publisher cancels the execution in progress.
    • PublishContext.
      • The second argument can be a [PublishContext](. /pipeline/publish-context) as the second argument.

e.g) The following signatures are all valid

Translated with www.DeepL.com/Translator (free version)

// public accessibility instead of `[Route]`
public void On(FooCommand cmd) { /* .. */ }

// Method name can be anything
[Route]
void Handle(FooCommand cmd) { /* ... */ }

// With CancellationToken
[Route]
async ValueTask On(FooCommand cmd, CancellationToken cancellation) { /* .. */ }

// With PublishContext
[Route]
void On(FooCommand cmd, PublishContext context) { /* .. */ }

// With UniTask
[Route]
async UniTask On(FooCommand cmd) { /* .. */ }

Use the MapTo method to initiate a subscription to a command. (This method is generated by the SourceGenerator)

var p = new FooPresenter();
p.MapTo(Router.Default);

Here we have specified Router.Default as the subscription destination.

The following will cause the command to be published.

await Router.Default.PublishAsync(new FooCommand());

Executing the above will call FooPresenter.On(FooCommand).

Incidentally, await PublishAsync will wait for all destinations to complete their [Route] methods.

To unsubscribe, either Dispose the return value of MapTo or call UnmapRoutes() to unsubscribe all.

Subscription s = p.MapTo();
s.Dispose(); // Unmap

// Or, unmap all routes
p.UnmapRoutes();

If your class inherits from MonoBehaviour in Unity, it will be automatically unsubscribed upon Destroy. Therefore, it is easier to call MapTo in Start() about MonoBehaviour.

[Routes] // < If routing as a MonoBehaviour
public class FooPresenter : MonoBehaviour
{
void Start()
{
MapTo(Router.Default); // < Start command handling here
}
}
tip

When using DI (Dependency Injection), you can leave the life cycle management, including map/unmap, to the DI container. See also the DI section.

This is the basic declarative routing mechanism. In addition, you can also make more complex additional settings using class declarations.

CommandOrdering

What happens if PublishAsync is executed in parallel before your async method completes?

_ = router.PublishAsync(new FooCommand()); // Fire and forget..
_ = router.PublishAsync(new FooCommand()); // Fire and forget..
_ = router.PublishAsync(new FooCommand()); // Fire and forget..

By default, the next command is delivered before the async method completes. To configure the behavior in this case, specify CommandOrdering.

[Routes(CommandOrdering.Sequential)] // < Order control of the commands delivered to this type.
public partial class FooPresentor
{
async ValueTask On(FooCommand cmd)
{
// ...
}
}

In this example, we have set CommandOrdering.Sequential. The delivery of the next command will be held off until On(FooCommand) has completed. This behavior is a powerful way to build conversation and scenario sequences in games.

For more information on CommandOrdering, please refer to the Sequencial Control section.

Filter

By adding the [Filter] attribute, you can insert processing before and after commands are delivered to the method.

note

This is a concept similar to middleware in web application frameworks and interceptors in gRPC. If you are familiar with server-side programming, I think it will be quite easy to get to grips with.

[Filter] can be added to class declarations or to individual methods.

[Routes]
[Filter(typeof(Filter1))] // < Class-wide filter
public partial class FooPresenter
{
[Route]
[Filter(typeof(Filter2))] // < Filter by each method
async ValueTask On(FooCommand)
{
// ...
}
}

For more information about the Filter, please refer to here.

Another router instance

It is possible to create multiple Router instances.

var router = new Router();

// Router has some interfaces.
ISubscribable subscribable = router;
IPublisher publisher = router;

p.MapTo(subscribable);

await publisher.PublishAsync(new FooCommand());

The cost of instantiating a Router is small. There is no problem with creating and using countless Routers within a project.