Request Handling¶
AlexaVoxCraft provides a powerful request handling system built on MediatR patterns, enabling CQRS-style request processing with type safety, auto-discovery, and comprehensive pipeline support.
🎯 Trivia Skill Examples: All code examples in this documentation demonstrate building a trivia game skill where users answer multiple-choice questions to test their knowledge.
Features¶
MediatR Integration: Built on proven CQRS patterns with MediatR
Auto-Discovery: Automatic handler registration from assemblies
Type Safety: Compile-time request/response validation
Pipeline Behaviors: Composable request/response processing
Exception Handling: Centralized error handling and recovery
Observability: Built-in logging and telemetry support
Basic Usage¶
Handler Registration¶
// In your AlexaSkillFunction
protected override void Init(IHostBuilder builder)
{
builder
.UseHandler<LambdaHandler, APLSkillRequest, SkillResponse>()
.ConfigureServices((context, services) =>
{
// Auto-register all handlers from assembly
services.AddSkillMediator(context.Configuration, cfg =>
cfg.RegisterServicesFromAssemblyContaining<Program>());
});
}
Simple Request Handler¶
using AlexaVoxCraft.MediatR;
using AlexaVoxCraft.Model.Request.Type;
using AlexaVoxCraft.Model.Response;
public class LaunchRequestHandler : IRequestHandler<LaunchRequest>
{
public bool CanHandle(IHandlerInput handlerInput) =>
handlerInput.RequestEnvelope.Request is LaunchRequest;
public async Task<SkillResponse> Handle(IHandlerInput input, CancellationToken cancellationToken)
{
return await input.ResponseBuilder
.Speak("Welcome to my skill!")
.WithShouldEndSession(false)
.GetResponse(cancellationToken);
}
}
Handler Types¶
Launch Request Handler¶
Handles skill launch and start-over intents:
public class LaunchRequestHandler : IRequestHandler<LaunchRequest>, IRequestHandler<IntentRequest>
{
private readonly IGameService _gameService;
private readonly IVisualBuilder _visualBuilder;
public LaunchRequestHandler(IGameService gameService, IVisualBuilder visualBuilder)
{
_gameService = gameService;
_visualBuilder = visualBuilder;
}
public Task<bool> CanHandle(IHandlerInput handlerInput, CancellationToken cancellationToken = default)
{
return Task.FromResult(handlerInput.RequestEnvelope.Request is LaunchRequest ||
(handlerInput.RequestEnvelope.Request is IntentRequest intent &&
intent.Intent.Name == BuiltInIntent.StartOver));
}
public async Task<SkillResponse> Handle(IHandlerInput input, CancellationToken cancellationToken)
{
var sessionAttributes = await input.AttributesManager.GetSessionAttributes(cancellationToken);
await _gameService.StartNewGame(cancellationToken);
var speechOutput = "Welcome to Trivia Challenge! Ready to test your knowledge?";
var repromptText = "Say yes to start, or help for instructions.";
if (input.RequestEnvelope.APLSupported())
{
var (renderDirective, executeDirective) = _visualBuilder
.AddWelcomeSlide(speechOutput, "show me the high scores")
.GetDirectives();
input.ResponseBuilder
.AddDirective(renderDirective)
.AddDirective(executeDirective);
}
return await input.ResponseBuilder
.Speak(speechOutput)
.Reprompt(repromptText)
.WithSimpleCard("Trivia Challenge", speechOutput)
.GetResponse(cancellationToken);
}
}
Intent Request Handler¶
Handles specific intents with slot processing:
public class AnswerHandler : IRequestHandler<IntentRequest>, IRequestHandler<UserEventRequest>
{
private readonly IGameService _gameService;
private readonly ILogger<AnswerHandler> _logger;
public AnswerHandler(IGameService gameService, ILogger<AnswerHandler> logger)
{
_gameService = gameService;
_logger = logger;
}
public Task<bool> CanHandle(IHandlerInput input, CancellationToken cancellationToken = default)
{
return Task.FromResult(input.RequestEnvelope.Request is UserEventRequest ||
input.RequestEnvelope.Request is IntentRequest intent &&
(intent.Intent.Name == "AnswerIntent" ||
intent.Intent.Name == "DontKnowIntent" ||
intent.Intent.Name == BuiltInIntent.Next));
}
public async Task<SkillResponse> Handle(IHandlerInput input, CancellationToken cancellationToken)
{
var submittedAnswer = GetAnswer(input.RequestEnvelope.Request);
var result = await _gameService.ProcessAnswer(submittedAnswer, cancellationToken);
var speechOutput = result.IsCorrect
? "Correct! Well done."
: $"Sorry, that's incorrect. The correct answer was {result.CorrectAnswer}.";
if (result.IsGameComplete)
{
// Game over logic
speechOutput += $" Game over! Your final score is {result.FinalScore} out of {result.TotalQuestions}.";
return await input.ResponseBuilder
.Speak(speechOutput)
.WithShouldEndSession(true)
.GetResponse(cancellationToken);
}
// Continue with next question
var nextQuestion = await _gameService.GetNextQuestion(cancellationToken);
var questionPrompt = BuildQuestionPrompt(nextQuestion);
return await input.ResponseBuilder
.Speak(speechOutput + " " + questionPrompt)
.Reprompt(questionPrompt)
.GetResponse(cancellationToken);
}
private int GetAnswer(Request request)
{
return request switch
{
IntentRequest intentRequest => GetAnswerFromSlot(intentRequest.Intent),
UserEventRequest eventRequest => GetAnswerFromEvent(eventRequest.Arguments),
_ => 0
};
}
private int GetAnswerFromSlot(Intent? intent)
{
if (intent?.Slots == null || !intent.Slots.ContainsKey("Answer"))
return 0;
if (int.TryParse(intent.Slots["Answer"].Value, out var answer) && answer is > 0 and <= 4)
return answer;
return 0;
}
private string BuildQuestionPrompt(QuestionData question)
{
var prompt = $"Question {question.Number}: {question.Text} ";
for (int i = 0; i < question.Choices.Count; i++)
{
prompt += $"{i + 1}. {question.Choices[i]}. ";
}
return prompt;
}
}
Base Handler Pattern¶
Create base handlers for common functionality:
public abstract class BaseGameHandler
{
protected const int GameLength = 5;
protected const int AnswerCount = 4;
protected readonly ILogger Logger;
protected readonly IGameService GameService;
protected readonly IVisualBuilder VisualBuilder;
protected BaseGameHandler(ILogger logger, IGameService gameService, IVisualBuilder visualBuilder)
{
Logger = logger ?? throw new ArgumentNullException(nameof(logger));
GameService = gameService ?? throw new ArgumentNullException(nameof(gameService));
VisualBuilder = visualBuilder ?? throw new ArgumentNullException(nameof(visualBuilder));
}
public abstract Task<bool> CanHandle(IHandlerInput input, CancellationToken cancellationToken = default);
public abstract Task<SkillResponse> Handle(IHandlerInput input, CancellationToken cancellationToken = default);
protected async Task<SkillResponse> StartGame(bool isNewGame, IHandlerInput handlerInput, CancellationToken cancellationToken)
{
var sessionAttributes = await handlerInput.AttributesManager.GetSessionAttributes(cancellationToken);
var speechOutput = new StringBuilder();
if (isNewGame)
{
speechOutput.AppendFormat("Welcome to {0}! ", "Trivia Challenge");
speechOutput.AppendFormat("I'll ask you {0} questions. ", GameLength);
}
var gameState = await GameService.StartNewGame(cancellationToken);
var questionPrompt = BuildQuestionPrompt(gameState.CurrentQuestion);
speechOutput.Append(questionPrompt);
sessionAttributes["speechOutput"] = questionPrompt;
sessionAttributes["repromptText"] = questionPrompt;
await handlerInput.AttributesManager.SetSessionAttributes(sessionAttributes, cancellationToken);
return await handlerInput.ResponseBuilder
.Speak(speechOutput.ToString())
.Reprompt(questionPrompt)
.WithSimpleCard("Trivia Challenge", questionPrompt)
.GetResponse(cancellationToken);
}
private string BuildQuestionPrompt(QuestionData question)
{
var prompt = new StringBuilder();
prompt.AppendFormat("Question {0}: {1} ",
question.Number,
question.Text);
for (int i = 0; i < question.Choices.Count; i++)
{
prompt.AppendFormat("{0}. {1}. ", i + 1, question.Choices[i]);
}
return prompt.ToString();
}
}
// Usage in derived handlers
public class LaunchRequestHandler : BaseGameHandler, IRequestHandler<LaunchRequest>
{
public LaunchRequestHandler(ILogger<LaunchRequestHandler> logger,
IGameService gameService,
IVisualBuilder visualBuilder)
: base(logger, gameService, visualBuilder)
{
}
public override Task<bool> CanHandle(IHandlerInput handlerInput, CancellationToken cancellationToken = default)
{
return Task.FromResult(handlerInput.RequestEnvelope.Request is LaunchRequest);
}
public override Task<SkillResponse> Handle(IHandlerInput handlerInput, CancellationToken cancellationToken = default)
{
return StartGame(true, handlerInput, cancellationToken);
}
}
Built-in Handlers¶
Session Ended Handler¶
public class SessionEndedHandler : IRequestHandler<SessionEndedRequest>
{
private readonly IGameService _gameService;
private readonly ILogger<SessionEndedHandler> _logger;
public SessionEndedHandler(IGameService gameService, ILogger<SessionEndedHandler> logger)
{
_gameService = gameService;
_logger = logger;
}
public Task<bool> CanHandle(IHandlerInput handlerInput, CancellationToken cancellationToken = default)
{
return Task.FromResult(handlerInput.RequestEnvelope.Request is SessionEndedRequest);
}
public async Task<SkillResponse> Handle(IHandlerInput input, CancellationToken cancellationToken)
{
_logger.LogInformation("Session ended: {reason}",
((SessionEndedRequest)input.RequestEnvelope.Request).Reason);
// Save any pending game state
await _gameService.SaveGameProgress(cancellationToken);
return await input.ResponseBuilder.GetResponse(cancellationToken);
}
}
Exception Handler¶
public class ErrorHandler : IExceptionHandler
{
private readonly ILogger<ErrorHandler> _logger;
public ErrorHandler(ILogger<ErrorHandler> logger)
{
_logger = logger;
}
public Task<bool> CanHandle(IHandlerInput handlerInput, Exception ex, CancellationToken cancellationToken)
{
return Task.FromResult(true); // Handle all exceptions
}
public Task<SkillResponse> Handle(IHandlerInput handlerInput, Exception ex, CancellationToken cancellationToken)
{
_logger.LogError(ex, "Unhandled exception in skill");
var speechText = "Sorry, I had trouble doing what you asked. Please try again.";
return handlerInput.ResponseBuilder
.Speak(speechText)
.Reprompt(speechText)
.GetResponse(cancellationToken);
}
}
Request Processing Pipeline¶
Request Flow¶
- Lambda Entry: Request enters through
LambdaHandler.HandleAsync
- Request Interceptors: Pre-processing (authentication, logging, state loading)
- Handler Resolution: MediatR resolves appropriate handler based on
CanHandle
logic - Handler Execution: Handler processes request and generates response
- Response Interceptors: Post-processing (state saving, cleanup, telemetry)
- Response Return: Final response sent to Alexa
Lambda Handler Implementation¶
public class LambdaHandler : ILambdaHandler<APLSkillRequest, SkillResponse>
{
private readonly ISkillMediator _mediator;
private readonly ILogger<LambdaHandler> _logger;
public LambdaHandler(ISkillMediator mediator, ILogger<LambdaHandler> logger)
{
_mediator = mediator ?? throw new ArgumentNullException(nameof(mediator));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
public async Task<SkillResponse> HandleAsync(APLSkillRequest request, ILambdaContext context)
{
using var activity = Activity.StartActivity($"{nameof(LambdaHandler)}.{nameof(HandleAsync)}");
if (request.Request is IntentRequest intent)
{
_logger.LogDebug("Received intent {intentType}", intent.Intent.Name);
activity?.SetTag("alexa.intent.name", intent.Intent.Name);
}
_logger.LogDebug("Received request of type {requestType}", request.Request.Type);
try
{
var response = await _mediator.Send(request);
activity?.SetStatus(ActivityStatusCode.Ok);
return response;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error handling request");
activity?.SetStatus(ActivityStatusCode.Error, ex.Message);
throw;
}
}
}
Handler Registration¶
Automatic Registration¶
// Register all handlers from assembly
services.AddSkillMediator(context.Configuration, cfg =>
cfg.RegisterServicesFromAssemblyContaining<Program>());
Manual Registration¶
services.AddSkillMediator(context.Configuration, cfg =>
{
cfg.RegisterHandlers(typeof(LaunchRequestHandler).Assembly);
cfg.RegisterInterceptors(typeof(GameManagerRequestInterceptor).Assembly);
cfg.RegisterExceptionHandlers(typeof(ErrorHandler).Assembly);
});
Best Practices¶
1. Use Dependency Injection¶
public class MyHandler : IRequestHandler<LaunchRequest>
{
private readonly IMyService _service;
private readonly ILogger<MyHandler> _logger;
public MyHandler(IMyService service, ILogger<MyHandler> logger)
{
_service = service;
_logger = logger;
}
}
2. Implement Proper Cancellation¶
public async Task<SkillResponse> Handle(IHandlerInput input, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
var data = await _service.GetDataAsync(cancellationToken);
return await input.ResponseBuilder
.Speak("Response")
.GetResponse(cancellationToken);
}
3. Use Structured Logging¶
_logger.LogDebug("Processing {requestType} for user {userId}",
request.GetType().Name,
input.RequestEnvelope.GetUserId());
4. Handle Slot Validation¶
private string? GetSlotValue(Intent intent, string slotName)
{
if (intent.Slots?.TryGetValue(slotName, out var slot) == true)
{
return slot.Value;
}
return null;
}
Examples¶
For more real-world examples, see the Examples section which includes the complete Disney Trivia skill implementation.