Handlers
Handlers are the central mechanism for encapsulation of responsibility and functionality in StoneFruit.
Basic Control Flow
Control flow in StoneFruit starts with an Engine:
- The
Engine
runloop gets an input string of text from the user - The string of text is parsed into an
IArguments
object - The
IArguments
object is passed to theCommandDispatcher
- The
CommandDispatcher
looks up the appropriate Handler for the command - The Handler is constructed using dependency injection and executed
- Control flow returns to the runloop to get the next input from the sources.
Important Objects
There are several objects in StoneFruit which will be important for the development of your handlers, each of which can be injected into the handler:
StoneFruit.StoneFruitApplication
is the execution engine which runs the application. The applicaiton gathers input commands from a variety of sources and dispatches them to the appropriate handlers.StoneFruit.EngineState
is a top-level context object to hold information about the state of the engine. The EngineState object will allow you to exit the engine runloop, store metadata values between commands, or schedule additional commands to execute.StoneFruit.IOutput
is an abstraction over output. By default output is sent to theSystem.Console
, and helper methods are provided to assist with color and formatting.StoneFruit.IInput
is an abstraction over user input. By default it prompts from the console. In headless mode there is no input.StoneFruit.IHandlerSource
is used to find handlers. There are several types of built-in sources, and also sources which wrap calls to your DI container (if you’re using one)StoneFruit.IArguments
holds the parsed arguments of a command, and can be used to access them or map them to objects. Internally, this object holds the parsed verb as well, though the verb will be removed before the handler is executed.StoneFruit.CommandDispatcher
takesIArguments
, finds a suitable handler, and invokes it.StoneFruit.IEnvironments
is used to hold environments. An environment is a custom object, whose lifespan is somewhere between an EngineState and a Command. Environments are usually used to provide access to application configuration data.
Types of Handlers
Handlers can take several forms:
- Classes which implement one of the interfaces
IHandler
orIAsyncHandler
- The public named methods on an object instance
- Anonymous
Action<HandlerContext>
or asynchronousFunc<HandlerContext, CancellationToken, Task>
delegates - Scripted combinations of other handlers
StoneFruit comes with a few built-in handlers for basic operations, but most of the interesting handlers will be developed by you. The built-in help
verb will list all the verbs currently registered with the StoneFruit engine.
Handler Classes
Here is an example of a synchronous handler class:
[Verb("example")]
public class MyExampleHandler : IHandler
{
private readonly IArguments _args;
private readonly IOutput _output;
public MyExampleHandler(IArguments args, IOutput output)
{
_args = args;
_output = output;
}
public static string Description => "...";
public static string Usage => "...";
public void Execute()
{
...
}
}
A new handler instance is created for every invocation, with the requested objects injected into the constructor. You can request any of the “Important Objects” listed above or any other type registered with your DI container (if you are using a container). The Execute()
method is where work happens. The [Verb]
attribute allows you to specify which verb(s) to associate with your handler. If you do not specify a [Verb]
attribute, the name of the handler will be used to create a verb for you (MyExampleHandler
=> "my example"
).
Here is an example of an asynchronous handler class, identical in function to the above example:
[Verb("example")]
public class MyExampleHandler : IAsyncHandler
{
private readonly IArguments _args;
private readonly IOutput _output;
public MyExampleHandler(IArguments args, IOutput output)
{
_args = args;
_output = output;
}
public static string Description => "";
public static string Usage => "";
public async Task ExecuteAsync(CancellationToken token)
{
...
}
}
The CancellationToken
is constrained by a timeout configurable during engine setup. It is good practice to check the token periodically if you are executing a long-running command.
Description and Usage
The special static string properties Description
and Usage
will display a short description and detailed usage information, respectively, in the output of the help
built-in command. If you do not provide these properties, no information will be shown.
Registering Handler Classes
Handler classes can be added to the system in two ways:
- Registered with the DI container using the
.AddHandler()
method, or - Create an instance and add it to the engine manually.
builder.SetupHandlers(h => h
// Add an existing instance
.Add(new MyHandler())
// Register the type with DI
.Add<MyHandler>()
);
builder.Services.AddHandler<MyHandler>();
In addition you can scan an assembly for public IHandler
types and have them added to DI automatically.
builder.SetupHandlers(h => h
// Scan this assembly
.ScanAssemblyForHandlers(assembly)
// Scan the entry assembly
.ScanHandlersFromEntryAssembly()
// Scan the current assembly
.ScanHandlersFromCurrentAssembly()
// Scan the assembly where MyType is defined
.ScanHandlersFromAssemblyContaining<MyType>()
);
Named Methods as Handlers
You can use the named public methods of an object as handlers:
services.SetupEngine(b => b
.SetupHandlers(h => h.UsePublicMethodsAsHandlers(new MyObject()))
);
For each public method, if the return type is void
it will be invoked synchronously and if it is Task
it will be invoked asynchronously. StoneFruit will attempt to fill in method parameters from the DI container and from values in the IArguments
object. If an unknown type is found, a default value will be provided instead. Here is some example code:
public class MyObject
{
public void TestA(IOutput output)
{
output.WriteLine("A");
}
public void TestB(IOutput output, string name)
{
output.WriteLine($"Name: {name}");
}
public Task TestC(IOutput output)
{
output.WriteLine("C");
return Task.CompletedTask;
}
}
Function Delegates as Handlers
You can use lambdas and function delegates as handlers:
services.SetupEngine(b => b
// synchronous
.SetupHandlers(h => h.Add("example", c => { ... }))
// asynchronous
.SetupHandlers(h => h.Add("example", async (c, token) => { ... }))
);
The delegate should be assignable to Action<HandlerContext>
for synchronous or Func<HandlerContext, CancellationToken, Task>
for asynchronous. The HandlerContext
object includes several important internal objects which can provide basic functionality.