AlexaVoxCraft is a modular C# .NET library for building Amazon Alexa skills using modern .NET practices. It provides comprehensive support for Alexa skill development with CQRS patterns, visual interfaces, and AWS Lambda hosting.
- 🎯 MediatR Integration: CQRS-style request handling with compile-time source-generated DI registration
- 🎨 APL Support: Complete Alexa Presentation Language implementation for rich visual interfaces
- ⚡ Lambda Hosting: Optimized AWS Lambda runtime with custom serialization and ReadyToRun publishing
- 📊 Session Management: Robust session attribute handling with typed attribute serialization
- 🔧 Pipeline Behaviors: Request/response interceptors for cross-cutting concerns like logging and validation
- 💰 In-Skill Purchasing: Full ISP support with buy/upsell/cancel directives and entitlement checks
- 🧪 Testing Support: Comprehensive testing utilities with AutoFixture integration and property-based testing
# Minimal Lambda hosting and MediatR integration
dotnet add package AlexaVoxCraft.MinimalLambda
dotnet add package AlexaVoxCraft.MediatR
# APL visual interface support (optional)
dotnet add package AlexaVoxCraft.Model.Apl
# OpenTelemetry observability (optional)
dotnet add package AlexaVoxCraft.Observability
# In-Skill Purchasing support (optional)
dotnet add package AlexaVoxCraft.InSkillPurchasing
# CloudWatch-compatible JSON logging (optional)
dotnet add package LayeredCraft.Logging.CompactJsonFormatterSupported target frameworks: .NET 8.0, .NET 9.0, .NET 10.0.
# Check your SDK version
dotnet --version
# Should show 8.0.400 or higher// Program.cs (MinimalLambda host)
using AlexaVoxCraft.Lambda.Abstractions;
using AlexaVoxCraft.MediatR.DI;
using AlexaVoxCraft.MinimalLambda;
using AlexaVoxCraft.MinimalLambda.Extensions;
using AlexaVoxCraft.Model.Response;
using Amazon.Lambda.Core;
using MinimalLambda.Builder;
var builder = LambdaApplication.CreateBuilder();
// Handlers are automatically discovered and registered at compile time
builder.Services.AddSkillMediator(builder.Configuration);
// Register AlexaVoxCraft hosting services and handler
builder.Services.AddAlexaSkillHost<LambdaHandler, SkillRequest, SkillResponse>();
await using var app = builder.Build();
app.MapHandler(AlexaHandler.Invoke<SkillRequest, SkillResponse>);
return await app.RunAsync();
// Lambda handler bridges MediatR to the MinimalLambda host
public class LambdaHandler : ILambdaHandler<SkillRequest, SkillResponse>
{
private readonly ISkillMediator _mediator;
public LambdaHandler(ISkillMediator mediator) => _mediator = mediator;
public Task<SkillResponse> HandleAsync(SkillRequest request, ILambdaContext context, CancellationToken cancellationToken) =>
_mediator.Send(request, cancellationToken);
}
// Handler.cs
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!")
.Reprompt("What would you like to do?")
.GetResponse(cancellationToken);
}
}📚 Complete Documentation - Comprehensive guides and examples
- Request Handling - MediatR integration and handler patterns
- Source Generation - Compile-time DI registration with C# interceptors
- APL Integration - Rich visual interface development
- Lambda Hosting - AWS Lambda deployment and optimization
- Session Management - State persistence and user data
- Pipeline Behaviors - Cross-cutting concerns and interceptors
- Complete Examples - Production-ready trivia skill implementation
AlexaVoxCraft/
├── 📂 src/ # Core library packages
│ ├── 📦 AlexaVoxCraft.Model/ # Base Alexa skill models & serialization
│ ├── 📦 AlexaVoxCraft.Model.Apl/ # APL (Alexa Presentation Language) support
│ ├── 📦 AlexaVoxCraft.MediatR/ # MediatR integration & request handling
│ ├── 📦 AlexaVoxCraft.MediatR.Generators/ # Source generator for compile-time DI
│ ├── 📦 AlexaVoxCraft.Lambda/ # Core Lambda abstractions & serialization
│ ├── 📦 AlexaVoxCraft.MinimalLambda/ # MinimalLambda-based hosting for Alexa skills
│ ├── 📦 AlexaVoxCraft.MediatR.Lambda/ # Legacy Lambda hosting (AlexaSkillFunction)
│ ├── 📦 AlexaVoxCraft.Observability/ # OpenTelemetry instrumentation & telemetry
│ ├── 📦 AlexaVoxCraft.Smapi/ # Skill Management API (SMAPI) client
│ ├── 📦 AlexaVoxCraft.Model.InSkillPurchasing/ # ISP model objects: directives, response types, payment types
│ └── 📦 AlexaVoxCraft.InSkillPurchasing/ # ISP runtime client (IInSkillPurchasingClient) for entitlement checks
│
├── 📂 samples/ # Working example projects
│ ├── 📱 Sample.Skill.Function/ # Basic skill (legacy hosting)
│ ├── 📱 Sample.Host.Function/ # Modern minimal API hosting
│ ├── 📱 Sample.Generated.Function/ # Source-generated DI demonstration
│ ├── 📱 Sample.Apl.Function/ # APL skill with visual interfaces
│ └── 📱 Sample.Fact.InSkill.Purchases/ # Premium Fact skill demonstrating ISP buy/upsell/cancel flows
│
├── 📂 test/ # Comprehensive test coverage
│ ├── 🧪 AlexaVoxCraft.Model.Tests/ # Core model & serialization tests
│ ├── 🧪 AlexaVoxCraft.Model.Apl.Tests/ # APL functionality tests
│ ├── 🧪 AlexaVoxCraft.MediatR.Tests/ # MediatR integration tests
│ ├── 🧪 AlexaVoxCraft.MediatR.Lambda.Tests/ # Lambda hosting tests
│ └── 🧪 AlexaVoxCraft.Smapi.Tests/ # SMAPI client tests
│
├── 📂 AlexaVoxCraft.TestKit/ # Testing utilities & AutoFixture support
└── 📂 docs/ # Documentation source
Skills use the MediatR pattern where:
- Requests implement
IRequestHandler<T> - Handlers optionally implement
ICanHandlefor routing logic - Pipeline behaviors handle cross-cutting concerns (logging, exceptions)
- Lambda functions derive from
AlexaSkillFunction<TRequest, TResponse>
| Package | Purpose | Key Features |
|---|---|---|
| AlexaVoxCraft.Model | Core Alexa models | Request/response types, SSML, cards, directives, System.Text.Json serialization |
| AlexaVoxCraft.Model.Apl | APL support | 40+ components, commands, audio, vector graphics, extensions (DataStore, SmartMotion) |
| AlexaVoxCraft.MediatR | Request handling | Handler routing, pipeline behaviors, attributes management, DI integration |
| AlexaVoxCraft.MinimalLambda | MinimalLambda hosting | Minimal API-style hosting for Alexa skills, custom serialization, handler mapping |
| AlexaVoxCraft.MediatR.Lambda | Legacy Lambda hosting | AWS Lambda functions, context management, custom serialization, hosting extensions |
| AlexaVoxCraft.Observability | OpenTelemetry integration | Opt-in telemetry, metrics, spans, semantic attributes, ADOT/CloudWatch support |
| AlexaVoxCraft.Model.InSkillPurchasing | ISP model objects | BuyDirective, UpsellDirective, CancelDirective, ConnectionResponsePayload, PaymentType |
| AlexaVoxCraft.InSkillPurchasing | ISP runtime client | IInSkillPurchasingClient with GetProductsAsync/GetProductAsync, DI registration via AddInSkillPurchasing() |
AlexaVoxCraft includes comprehensive testing support:
- xUnit v3 with Microsoft.Testing.Platform
- AutoFixture integration for property-based testing
- AwesomeAssertions for fluent assertions
- TestKit with specimen builders and test utilities
Implement the IExceptionHandler interface for centralized error handling:
public class GlobalExceptionHandler : IExceptionHandler
{
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)
{
return handlerInput.ResponseBuilder
.Speak("Sorry, something went wrong. Please try again.")
.GetResponse(cancellationToken);
}
}Version 4.0.0 introduces cancellation token support for Lambda functions. This is a breaking change that requires updates to your existing lambda handlers.
1. Update Lambda Handlers
Lambda handlers must now accept and pass the cancellation token:
// ❌ Before (v3.x)
public class LambdaHandler : ILambdaHandler<SkillRequest, SkillResponse>
{
public async Task<SkillResponse> HandleAsync(SkillRequest input, ILambdaContext context)
{
return await _skillMediator.Send(input);
}
}
// ✅ After (v4.0+)
public class LambdaHandler : ILambdaHandler<SkillRequest, SkillResponse>
{
public async Task<SkillResponse> HandleAsync(SkillRequest input, ILambdaContext context, CancellationToken cancellationToken)
{
return await _skillMediator.Send(input, cancellationToken);
}
}2. Update AlexaSkillFunction Override
If you override FunctionHandlerAsync in your skill function class, you must update the signature:
// ❌ Before (v3.x)
public class MySkillFunction : AlexaSkillFunction<SkillRequest, SkillResponse>
{
public override async Task<SkillResponse> FunctionHandlerAsync(SkillRequest input, ILambdaContext context)
{
// Custom logic here
return await base.FunctionHandlerAsync(input, context);
}
}
// ✅ After (v4.0+)
public class MySkillFunction : AlexaSkillFunction<SkillRequest, SkillResponse>
{
public override async Task<SkillResponse> FunctionHandlerAsync(SkillRequest input, ILambdaContext context, CancellationToken cancellationToken)
{
// Custom logic here
return await base.FunctionHandlerAsync(input, context, cancellationToken);
}
}You can now configure the cancellation timeout buffer in your appsettings.json:
{
"SkillConfiguration": {
"SkillId": "amzn1.ask.skill.your-skill-id",
"CancellationTimeoutBufferMilliseconds": 500
}
}The timeout buffer (default: 250ms) is subtracted from Lambda's remaining execution time to allow graceful shutdown and telemetry flushing.
Version 5.1.0 (beta) introduces the AlexaVoxCraft.MinimalLambda package, replacing the previous AlexaVoxCraft.Lambda.Host host with a MinimalLambda-based experience. The legacy hosting approach remains fully supported for backward compatibility.
Version 5.1.0 refines the hosting lineup:
| Package | Purpose | Status |
|---|---|---|
| AlexaVoxCraft.Lambda | Core Lambda abstractions and serialization | Active |
| AlexaVoxCraft.MinimalLambda | MinimalLambda-based hosting | New (recommended for new projects) |
| AlexaVoxCraft.MediatR.Lambda | Legacy Lambda hosting with AlexaSkillFunction | Existing (still fully supported) |
Core Lambda abstractions have been moved to the new AlexaVoxCraft.Lambda package:
Classes Moved:
ILambdaHandler<TRequest, TResponse>HandlerDelegate<TRequest, TResponse>AlexaLambdaSerializerSystemTextDestructuringPolicy
The following obsolete class has been removed:
PollyVoices - This class was marked as obsolete in previous versions and has been removed. Use the AlexaSupportedVoices class instead for Amazon Polly voice name constants.
Before (v4.x):
using AlexaVoxCraft.MediatR.Lambda.Abstractions;
using AlexaVoxCraft.MediatR.Lambda.Serialization;
public class LambdaHandler : ILambdaHandler<SkillRequest, SkillResponse>
{
// Implementation
}After (v5.0+):
using AlexaVoxCraft.Lambda.Abstractions;
using AlexaVoxCraft.Lambda.Serialization;
public class LambdaHandler : ILambdaHandler<SkillRequest, SkillResponse>
{
// Implementation
}Version 5.1.0 provides two ways to host Alexa skills in AWS Lambda:
Use AlexaVoxCraft.MinimalLambda for a familiar minimal API-style hosting experience:
dotnet add package AlexaVoxCraft.MinimalLambda// Program.cs
using AlexaVoxCraft.Lambda.Abstractions;
using AlexaVoxCraft.MediatR.DI;
using AlexaVoxCraft.MinimalLambda;
using AlexaVoxCraft.MinimalLambda.Extensions;
using AlexaVoxCraft.Model.Response;
using MinimalLambda.Builder;
var builder = LambdaApplication.CreateBuilder();
builder.Services.AddSkillMediator(builder.Configuration);
builder.Services.AddAlexaSkillHost<LambdaHandler, SkillRequest, SkillResponse>();
await using var app = builder.Build();
app.MapHandler(AlexaHandler.Invoke<SkillRequest, SkillResponse>);
await app.RunAsync();Benefits:
- Familiar minimal API-style builder pattern (similar to ASP.NET Core)
- Lean MinimalLambda runtime keeps cold starts small
- More flexible service configuration
- Better separation of concerns
Continue using AlexaVoxCraft.MediatR.Lambda with the AlexaSkillFunction pattern:
dotnet add package AlexaVoxCraft.MediatR.Lambda// Program.cs
using AlexaVoxCraft.MediatR.Lambda;
return await LambdaHostExtensions.RunAlexaSkill<MySkillFunction, SkillRequest, SkillResponse>();
// Function.cs
public class MySkillFunction : AlexaSkillFunction<SkillRequest, SkillResponse>
{
protected override void Init(IHostBuilder builder)
{
builder
.UseHandler<LambdaHandler, SkillRequest, SkillResponse>()
.ConfigureServices((context, services) =>
{
services.AddSkillMediator(context.Configuration);
});
}
}This approach remains fully supported and requires no migration for existing projects.
Who is affected:
- Projects directly referencing
AlexaVoxCraft.MediatR.Lambda.Abstractionsnamespace - Projects directly referencing
AlexaVoxCraft.MediatR.Lambda.Serializationnamespace - Projects wanting to adopt the modern hosting approach
Who is NOT affected:
- Projects using
AlexaSkillFunction<TRequest, TResponse>without directly referencing moved classes - Projects that don't import the moved namespaces
If you directly reference the moved classes, update your using statements:
// Change this:
using AlexaVoxCraft.MediatR.Lambda.Abstractions;
using AlexaVoxCraft.MediatR.Lambda.Serialization;
// To this:
using AlexaVoxCraft.Lambda.Abstractions;
using AlexaVoxCraft.Lambda.Serialization;To adopt the new minimal API-style hosting:
-
Replace package reference:
dotnet remove package AlexaVoxCraft.MediatR.Lambda dotnet add package AlexaVoxCraft.MinimalLambda dotnet add package AlexaVoxCraft.MediatR
-
Remove
AwsLambda.Hostinterceptor namespaces (not needed with MinimalLambda) and keep your generator interceptors. -
Refactor Program.cs to use builder pattern (see example above)
-
Remove Function class inheriting from
AlexaSkillFunction
For detailed migration guidance, see the Lambda Hosting documentation.
Version 6.0.0 introduces a major redesign of the APL collection type system with breaking changes that affect how you work with APL component arrays and collections.
1. Migration from APLValue<IList> to APLValueCollection
All properties that previously used APLValue<IList<T>> or APLValue<List<T>> have been migrated to APLValueCollection<T>:
// ❌ Before (v5.x)
public APLValue<IList<APLComponent>>? Items { get; set; }
public APLValue<List<int>>? Padding { get; set; }
// ✅ After (v6.0.0+)
public APLValueCollection<APLComponent>? Items { get; set; }
public APLValueCollection<int>? Padding { get; set; }2. APLValue No Longer Supports Collection Types
APLValue<T> now throws InvalidOperationException in its static constructor if T is any collection type (IEnumerable, ICollection, IList, List, etc.):
// ❌ This will throw InvalidOperationException at runtime
var value = new APLValue<List<string>>();
// ✅ Use APLValueCollection<T> instead
var collection = new APLValueCollection<string>();3. APLValueCollection Implements IList
APLValueCollection<T> now implements IList<T> and IReadOnlyList<T> directly, providing natural collection ergonomics:
// ❌ Before (v5.x) - awkward .Items property access
collection.Items!.Add(new Text());
var count = collection.Items?.Count ?? 0;
var first = collection.Items![0];
// ✅ After (v6.0.0+) - direct collection operations
collection.Add(new Text());
var count = collection.Count;
var first = collection[0];4. Items Property Changed to Read-Only
The Items property is now IReadOnlyList<T> (read-only) instead of IList<T>? (settable):
// ❌ Before (v5.x) - Items was settable
collection.Items = new List<APLComponent> { new Text() };
// ✅ After (v6.0.0+) - Items is read-only, use constructor or Add()
var collection = new APLValueCollection<APLComponent>([new Text()]);
// OR
collection.Add(new Text());5. Materialize Pattern for Expression Handling
Mutations automatically clear the Expression property when you modify the collection:
// Expression-backed collection (data binding)
APLValueCollection<APLComponent> collection = "${data.items}";
collection.Expression; // "${data.items}"
// Mutation materializes the collection
collection.Add(new Text());
collection.Expression; // null - expression cleared
// Read operations preserve the expression
var count = collection.Count; // Does NOT clear Expression
var contains = collection.Contains(item); // Does NOT clear ExpressionStep 1: Update Property Types
If you have custom APL components or are working with the model directly:
// Change this:
public APLValue<IList<APLComponent>>? Children { get; set; }
public APLValue<List<int>>? Padding { get; set; }
// To this:
public APLValueCollection<APLComponent>? Children { get; set; }
public APLValueCollection<int>? Padding { get; set; }Step 2: Remove .Items Property Access for Mutations
// ❌ Before (v5.x)
container.Items!.Add(new Text { Content = "Hello" });
container.Items!.Clear();
var component = container.Items![0];
// ✅ After (v6.0.0+)
container.Add(new Text { Content = "Hello" });
container.Clear();
var component = container[0];Step 3: Update Collection Initialization
Collections can be initialized using collection expressions (C# 12), constructors, or implicit conversions:
// Collection expression (C# 12+)
APLValueCollection<APLComponent> items = [
new Text { Content = "Item 1" },
new Text { Content = "Item 2" }
];
// Constructor with IEnumerable
var list = new List<APLComponent> { new Text() };
var collection = new APLValueCollection<APLComponent>(list);
// Implicit conversion
APLValueCollection<APLComponent> fromList = new List<APLComponent> { new Text() };
APLValueCollection<APLComponent> fromArray = new[] { new Text() };
APLValueCollection<APLComponent> fromExpression = "${data.items}";Step 4: Be Aware of Materialize Behavior
If you're using expression-based binding and then mutating the collection:
// This will clear the expression
var collection = new APLValueCollection<APLComponent> { Expression = "${data.items}" };
collection.Add(new Text()); // Expression is now null
// If you need to preserve expression mode, don't mutate the collection
// Use expression-only or items-only, not both- ✅ Collection expressions:
[item1, item2, item3] - ✅ LINQ operations:
collection.OfType<Text>().ToList() - ✅ Foreach enumeration:
foreach (var item in collection) - ✅ Implicit conversions from
List<T>,T[], andstring - ✅ Expression-based data binding:
"${data.items}" - ✅ JSON serialization (unchanged behavior)
- ✅ The
Itemsproperty for read-only access and inspection
- Natural API: Direct collection operations without
.Itemsindirection - Type Safety: Cannot accidentally use
APLValue<List<T>>anymore - Better IntelliSense: Collection methods appear directly on the type
- Clearer Intent: Materialize pattern makes expression/items relationship explicit
- Standard .NET Patterns: Implements
IList<T>like other collection types
High Impact:
- Custom APL component implementations
- Code directly manipulating APL collections
- Any code using
APLValue<IList<T>>orAPLValue<List<T>>
Low Impact:
- Most skill code using the document builder API
- Code that only reads from collections
- Expression-based data binding (still works)
Version 7.0.0 replaces the untyped Dictionary<string, object> attribute system with a System.Text.Json-native Dictionary<string, JsonElement> model throughout. This is a breaking change affecting model types, IAttributesManager, and IPersistenceAdapter.
1. Session.Attributes and SkillResponse.SessionAttributes — AlexaVoxCraft.Model
Both properties changed from Dictionary<string, object> to Dictionary<string, JsonElement>:
// ❌ Before (v6.x)
Dictionary<string, object> attributes = request.Session.Attributes;
Dictionary<string, object>? sessionAttributes = response.SessionAttributes;
// ✅ After (v7.0+)
Dictionary<string, JsonElement> attributes = request.Session.Attributes;
Dictionary<string, JsonElement>? sessionAttributes = response.SessionAttributes;Access values using the JsonElement API:
// ❌ Before (v6.x)
var score = (int)sessionAttributes["currentScore"];
// ✅ After (v7.0+)
var score = sessionAttributes["currentScore"].GetInt32();2. IAttributesManager — Complete Redesign — AlexaVoxCraft.MediatR
The old async get/set methods are replaced by synchronous JsonAttributeBag properties and explicit typed helpers:
// ❌ Before (v6.x)
var session = await input.AttributesManager.GetSessionAttributes(cancellationToken);
session["currentScore"] = 42;
await input.AttributesManager.SetSessionAttributes(session, cancellationToken);
var request = await input.AttributesManager.GetRequestAttributes(cancellationToken);
request["tempKey"] = "value";
var persistent = await input.AttributesManager.GetPersistentAttributes(cancellationToken);
persistent["totalGames"] = 10;
await input.AttributesManager.SetPersistentAttributes(persistent, cancellationToken);
// ✅ After (v7.0+)
// Session — synchronous, typed
input.AttributesManager.Session.Set("currentScore", 42);
var score = input.AttributesManager.Session.Get<int>("currentScore");
// Request — synchronous, typed
input.AttributesManager.Request.Set("tempKey", "value");
// Persistent — still async, returns JsonAttributeBag
var persistent = await input.AttributesManager.GetPersistentAsync(cancellationToken);
persistent.Set("totalGames", 10);
await input.AttributesManager.SavePersistentAttributes(cancellationToken);Shorthand typed session state methods are also available directly on IAttributesManager:
input.AttributesManager.SetSessionState("gameStarted", true);
var started = input.AttributesManager.GetSessionState<bool>("gameStarted");
if (input.AttributesManager.TryGetSessionState<GameState>("state", out var state))
{
// use state
}
input.AttributesManager.ClearSessionState("gameStarted");3. IPersistenceAdapter — AlexaVoxCraft.MediatR
Method signatures changed from IDictionary<string, object> to IDictionary<string, JsonElement>:
// ❌ Before (v6.x)
public interface IPersistenceAdapter
{
Task<IDictionary<string, object>> GetAttributes(SkillRequest requestEnvelope, CancellationToken cancellationToken = default);
Task SaveAttribute(SkillRequest requestEnvelope, IDictionary<string, object> attributes, CancellationToken cancellationToken = default);
}
// ✅ After (v7.0+)
public interface IPersistenceAdapter
{
Task<IDictionary<string, JsonElement>> GetAttributes(SkillRequest requestEnvelope, CancellationToken cancellationToken = default);
Task SaveAttribute(SkillRequest requestEnvelope, IDictionary<string, JsonElement> attributes, CancellationToken cancellationToken = default);
}Any existing IPersistenceAdapter implementations must update their return type and parameter type, and serialize/deserialize values as JsonElement.
4. Removed Types
The following type has been removed with no replacement:
| Removed Type | Package | Notes |
|---|---|---|
DictionaryExtensions |
AlexaVoxCraft.Model.Apl |
Internal APL helper; no public replacement needed. |
Step 1: Update IPersistenceAdapter implementations
// Change return and parameter types
public async Task<IDictionary<string, JsonElement>> GetAttributes(
SkillRequest requestEnvelope, CancellationToken cancellationToken = default)
{
// Deserialize stored data to Dictionary<string, JsonElement>
var json = await _store.GetAsync(requestEnvelope.Session.User.UserId, cancellationToken);
return string.IsNullOrEmpty(json)
? new Dictionary<string, JsonElement>()
: JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(json)!;
}
public async Task SaveAttribute(SkillRequest requestEnvelope,
IDictionary<string, JsonElement> attributes, CancellationToken cancellationToken = default)
{
var json = JsonSerializer.Serialize(attributes);
await _store.SaveAsync(requestEnvelope.Session.User.UserId, json, cancellationToken);
}Step 2: Replace GetSessionAttributes / SetSessionAttributes calls
// ❌ Before
var attrs = await input.AttributesManager.GetSessionAttributes(cancellationToken);
attrs.TryGetAttribute<int>("score", out var score);
attrs.SetAttribute("score", score + 1);
await input.AttributesManager.SetSessionAttributes(attrs, cancellationToken);
// ✅ After — session is auto-saved by DefaultResponseBuilder
input.AttributesManager.TryGetSessionState<int>("score", out var score);
input.AttributesManager.SetSessionState("score", score + 1);Step 3: Replace raw Session.Attributes access
// ❌ Before
var value = (int)request.Session.Attributes["score"];
var text = request.Session.Attributes["name"] as string;
// ✅ After
var value = request.Session.Attributes["score"].GetInt32();
var text = request.Session.Attributes["name"].GetString();High Impact:
- Custom
IPersistenceAdapterimplementations - Code reading from or writing to
Session.AttributesorSkillResponse.SessionAttributesdirectly - Code calling the old
IAttributesManagerget/set methods
Low Impact:
- Skills that only use
input.ResponseBuilderand don't directly inspect session attributes - Code using
input.AttributesManageronly for persistent attributes (API shape is preserved, types updated)
PRs are welcome! Please submit issues and ideas to help make this toolkit even better.
📦 Credits:
- Core Alexa skill models (
AlexaVoxCraft.Model) based on timheuer/alexa-skills-dotnet- APL support (
AlexaVoxCraft.Model.Apl) based on stoiveyp/Alexa.NET.APL
This project is licensed under the MIT License.