SMAPI (Skill Management API) client for programmatic Alexa skill management in CI/CD pipelines and external tooling.
$ dotnet add package AlexaVoxCraft.SmapiAlexaVoxCraft 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.
# 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.CompactJsonFormatter
Supported target frameworks: .NET 8.0, .NET 9.0, .NET 10.0.
⚠️ SDK Version Required: To use source-generated dependency injection with interceptors, you must use at least version 8.0.400 of the .NET SDK. This ships with Visual Studio 2022 version 17.11 or higher.
# 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
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:
IRequestHandler<T>ICanHandle for routing logicAlexaSkillFunction<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:
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>AlexaLambdaSerializerSystemTextDestructuringPolicyThe 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:
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:
AlexaVoxCraft.MediatR.Lambda.Abstractions namespaceAlexaVoxCraft.MediatR.Lambda.Serialization namespaceWho is NOT affected:
AlexaSkillFunction<TRequest, TResponse> without directly referencing moved classesIf 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.Host interceptor 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 Expression
Step 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
[item1, item2, item3]collection.OfType<Text>().ToList()foreach (var item in collection)List<T>, T[], and string"${data.items}"Items property for read-only access and inspection.Items indirectionAPLValue<List<T>> anymoreIList<T> like other collection typesHigh Impact:
APLValue<IList<T>> or APLValue<List<T>>Low Impact:
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:
IPersistenceAdapter implementationsSession.Attributes or SkillResponse.SessionAttributes directlyIAttributesManager get/set methodsLow Impact:
input.ResponseBuilder and don't directly inspect session attributesinput.AttributesManager only 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.