Instructor is a lightweight, minimalist library focused on one job: dispatching instructions (commands and queries) to their respective handlers.
It integrates seamlessly with your existing IoC container—like Autofac or Microsoft.Extensions.DependencyInjection—without enforcing conventions or adding unnecessary abstractions. All handler resolution is delegated to the container you already use.
A working example can be found in the Instructor GitHub Repository.
Note: The None type (aka Unit) shown below is not within the Instructor.Core package, its within the demo project.
Just implement IInstruction<TValue> for each instruction.
using Instructor.Core.Common.Seeds;
public class AddCustomerCommand: IInstruction<None>
{
public Guid CustomerID { get;}
public string CustomerName { get; } = default!;
public AddCustomerCommand(CustomerData customerData)
=> (CustomerID, CustomerName) = (customerData.CustomerID, customerData.CustomerName);
}
public class GetCustomerQuery(Guid customerID) : IInstruction<CustomerData>
{
public Guid CustomerID { get; } = customerID;
}
Implement IInstructionHandler<TInstruction, TValue> or optionally the marker interfaces ICommandHandler<TInstruction, TValue> or IQueryHandler<TInstruction, TValue>.
using Instructor.Core.Common.Seeds;
public class AddCustomerCommandHandler : ICommandHandler<AddCustomerCommand, None>
{
public AddCustomerCommandHandler(){ }//TODO: inject database dependencies here
public async Task<None> Handle(AddCustomerCommand instruction, CancellationToken cancellationToken)
{
await Console.Out.WriteLineAsync($"Added customer {instruction.CustomerName} with ID {instruction.CustomerID} to the database (not).");
return None.Value;
}
}
public class GetCustomerQueryHandler : IQueryHandler<GetCustomerQuery, CustomerData>
{
public GetCustomerQueryHandler() { } //TODO: inject dependencies here as usual
public async Task<CustomerData> Handle(GetCustomerQuery instruction, CancellationToken cancellationToken)
{
await Console.Out.WriteLineAsync($"Getting the customer with ID {instruction.CustomerID} from the database (not).");
return new CustomerData(instruction.CustomerID, "John Doe");
}
}
Register the InstructionDispatcher and its factory delegate that defers resolution of instruction handlers via the underlying IoC container
Autofcac example:
using Instructor.Core;
using Instructor.Core.Common.Seeds;
/*
* Scan assemblies or add handlers manually i.e. builder.RegisterType<AddCustomerCommandHandler>().As<IInstructionHandler<AddCustomerCommand, None>>()
*/
builder.RegisterAssemblyTypes(AppDomain.CurrentDomain.GetAssemblies()).AsClosedTypesOf(typeof(IInstructionHandler<,>));
builder.Register<InstructionDispatcher>(c =>
{
var context = c.Resolve<IComponentContext>();
return new InstructionDispatcher(type => context.Resolve(type));
}).As<IInstructionDispatcher>().InstancePerLifetimeScope();
Microsoft.Extensions.DependencyInjection example:
using Instructor.Core;
using Instructor.Core.Common.Seeds;
/*
* Add handlers manually or get a package like Scrutor to scan assemblies
*/
.Services.AddTransient<IInstructionHandler<AddCustomerCommand, None>, AddCustomerCommandHandler>()
.AddTransient<IInstructionHandler<GetCustomerQuery, CustomerData>, GetCustomerQueryHandler>()
.AddSingleton<IInstructionDispatcher>(provider => new InstructionDispatcher(type => provider.GetRequiredService(type)))
using Instructor.Core;
using Instructor.Core.Common.Seeds;
private readonly IInstructionDispatcher _instructionDispatcher;//set by constructor injection
var newCustomer = new CustomerData(Guid.NewGuid(), "John Doe");//Data recieved from some client
_ = await __instructionDispatcher.SendInstruction(new AddCustomerCommand(newCustomer));//just dispatch the command (instruction) for handling
var customerData = await __instructionDispatcher.SendInstruction(new GetCustomerQuery(newCustomer.CustomerID));