Session Management¶
AlexaVoxCraft provides a unified attribute management system for session, request, and persistent state in Alexa skills through IAttributesManager and the JsonAttributeBag type.
Features¶
- Session Attributes: Automatic hydration from the incoming request's session
- Request Attributes: Per-request in-memory state scoped to the current invocation
- Persistent Attributes: Lazily-loaded, adapter-backed storage (e.g. DynamoDB)
- Typed Access:
Get<T>,Set<T>,TryGet<T>, andGetRequired<T>on all attribute bags - JsonElement-Based: All values stored as
JsonElement, serialized viaSystem.Text.Json
IAttributesManager¶
IAttributesManager is injected into handlers and interceptors via IHandlerInput:
public interface IAttributesManager
{
JsonAttributeBag Session { get; }
JsonAttributeBag Request { get; }
Task<JsonAttributeBag> GetPersistentAsync(CancellationToken ct = default);
Task SavePersistentAttributes(CancellationToken cancellationToken = default);
Task<Session?> GetSession(CancellationToken cancellationToken = default);
}
JsonAttributeBag¶
JsonAttributeBag wraps Dictionary<string, JsonElement> with typed, serialization-aware access methods. All serialization uses AlexaJsonOptions.DefaultOptions.
// Typed write
bag.Set<int>("score", 42);
bag.Set<Product[]>("entitledProducts", products);
// Typed read (returns default if key missing)
var score = bag.Get<int>("score");
// Try pattern
if (bag.TryGet<Product[]>("entitledProducts", out var products))
{
// use products
}
// Required — throws KeyNotFoundException if missing
var state = bag.GetRequired<GameState>("gameState");
// Removal
bag.Remove("tempKey");
bag.Clear();
// Raw dictionary access (e.g. for assigning to SkillResponse.SessionAttributes)
Dictionary<string, JsonElement> raw = bag.Values;
Session Attributes¶
IAttributesManager.Session is a JsonAttributeBag initialized from the incoming request's Session.Attributes. It is synchronous and always available (empty bag if the session has no attributes).
public async Task<SkillResponse> Handle(IHandlerInput input, CancellationToken cancellationToken)
{
// Read
var score = input.AttributesManager.Session.Get<int>("currentScore");
// Write
input.AttributesManager.Session.Set("currentScore", score + 1);
return await input.ResponseBuilder
.Speak($"Your score is now {score + 1}.")
.GetResponse(cancellationToken);
}
The DefaultResponseBuilder automatically copies Session.Values into SkillResponse.SessionAttributes when building the response — no manual step required.
Request Attributes¶
IAttributesManager.Request is an empty JsonAttributeBag scoped to the current invocation. Use it to share computed data between interceptors and handlers without round-tripping through session.
// In a request interceptor
public async Task Process(IHandlerInput input, CancellationToken cancellationToken = default)
{
var products = await _productService.GetEntitledProductsAsync(cancellationToken);
input.AttributesManager.Request.Set("entitledProducts", products);
}
// In a handler
public async Task<SkillResponse> Handle(IHandlerInput input, CancellationToken cancellationToken)
{
var products = input.AttributesManager.Request.Get<Product[]>("entitledProducts");
// ...
}
Persistent Attributes¶
Persistent attributes require an IPersistenceAdapter implementation registered in DI. They are loaded lazily on first access and must be explicitly saved.
IPersistenceAdapter¶
public interface IPersistenceAdapter
{
Task<IDictionary<string, JsonElement>> GetAttributes(SkillRequest requestEnvelope,
CancellationToken cancellationToken = default);
Task SaveAttribute(SkillRequest requestEnvelope, IDictionary<string, JsonElement> attributes,
CancellationToken cancellationToken = default);
}
Example Implementation¶
public sealed class DynamoDbPersistenceAdapter : IPersistenceAdapter
{
private readonly IAmazonDynamoDB _client;
private readonly string _tableName;
public DynamoDbPersistenceAdapter(IAmazonDynamoDB client, string tableName)
{
_client = client;
_tableName = tableName;
}
public async Task<IDictionary<string, JsonElement>> GetAttributes(
SkillRequest requestEnvelope, CancellationToken cancellationToken = default)
{
var userId = requestEnvelope.Session?.User?.UserId
?? throw new InvalidOperationException("No user ID in request.");
var response = await _client.GetItemAsync(new GetItemRequest
{
TableName = _tableName,
Key = new Dictionary<string, AttributeValue>
{
{ "userId", new AttributeValue { S = userId } }
}
}, cancellationToken);
if (response.Item is null || response.Item.Count == 0)
return new Dictionary<string, JsonElement>();
// Deserialize stored JSON blob back to JsonElement dictionary
var json = response.Item["attributes"].S;
return JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(json)
?? new Dictionary<string, JsonElement>();
}
public async Task SaveAttribute(SkillRequest requestEnvelope,
IDictionary<string, JsonElement> attributes, CancellationToken cancellationToken = default)
{
var userId = requestEnvelope.Session?.User?.UserId
?? throw new InvalidOperationException("No user ID in request.");
var json = JsonSerializer.Serialize(attributes);
await _client.PutItemAsync(new PutItemRequest
{
TableName = _tableName,
Item = new Dictionary<string, AttributeValue>
{
{ "userId", new AttributeValue { S = userId } },
{ "attributes", new AttributeValue { S = json } }
}
}, cancellationToken);
}
}
Registration¶
services.AddSkillMediator(configuration, cfg =>
cfg.RegisterServicesFromAssemblyContaining<Program>());
services.AddSingleton<IPersistenceAdapter, DynamoDbPersistenceAdapter>();
Reading and Saving¶
public async Task<SkillResponse> Handle(IHandlerInput input, CancellationToken cancellationToken)
{
var persistent = await input.AttributesManager.GetPersistentAsync(cancellationToken);
var totalGames = persistent.Get<int>("totalGames");
persistent.Set("totalGames", totalGames + 1);
await input.AttributesManager.SavePersistentAttributes(cancellationToken);
return await input.ResponseBuilder
.Speak($"You have played {totalGames + 1} games total.")
.GetResponse(cancellationToken);
}
SavePersistentAttributes is a no-op if GetPersistentAsync was never called, preventing unnecessary writes.
Interceptor Pattern¶
Interceptors are the recommended place to load and save state, keeping handlers focused on response logic.
Request Interceptor¶
public sealed class LoadStateInterceptor : IRequestInterceptor
{
public async Task Process(IHandlerInput input, CancellationToken cancellationToken = default)
{
var persistent = await input.AttributesManager.GetPersistentAsync(cancellationToken);
input.AttributesManager.Request.Set("playerProfile", persistent.Get<PlayerProfile>("profile"));
}
}
Response Interceptor¶
public sealed class SaveStateInterceptor : IResponseInterceptor
{
public async Task Process(IHandlerInput input, SkillResponse response,
CancellationToken cancellationToken = default)
{
var profile = input.AttributesManager.Request.Get<PlayerProfile>("playerProfile");
var persistent = await input.AttributesManager.GetPersistentAsync(cancellationToken);
persistent.Set("profile", profile);
await input.AttributesManager.SavePersistentAttributes(cancellationToken);
}
}
Checking Session State in CanHandle¶
public Task<bool> CanHandle(IHandlerInput handlerInput, CancellationToken cancellationToken = default)
{
var isNewSession = handlerInput.RequestEnvelope.Session?.New == true;
var hasGameStarted = handlerInput.AttributesManager.Session.TryGet<bool>("gameStarted", out var started)
&& started;
return Task.FromResult(isNewSession || !hasGameStarted);
}
Raw Session Object¶
Use GetSession when you need the full Session model (e.g. user ID, application ID):