In this Alexa.Tip series, we explore some little bits of code that can make your life easier in developing Alexa Skills in many languages including C# + .NET, node.jS + TypeScript, Kotlin, etc. We look at concepts, design patterns, and implementations that developers might not be aware of, and how they can be applied to voice application development, best practices, and more!
In this post, we explore some more best practices in developing Alexa Skills in C# whether you are using an ASP.NET Core API or an AWS Lambda. This time, we talk about taking our Handler Registration Pattern to the next level by using dependencies in them such as EF DbContexts
or really any other service you want to inject.
Check out all the raw source code for this post, and more here: https://github.com/SuavePirate/Alexa.Tips.Net
If you haven’t read up on how to use the Handler Registration Pattern, take a look at my earlier post here: Alexa.Tip – Using Handler Registration Pattern in .NET
The short version is that we use this pattern of registering IHandler
implementations to handle different types of requests that our skill receives, regardless of whether we are using AWS Lambdas or ASP.NET Core APIs.
Building Handlers with Dependencies
So we’ve seen samples of simple Handlers
that use static responses simply using the ResponseBuilder
from the Alexa.NET Library, but now let’s take it to the next logical step and start to get some data from a database. In this case, I am using Entity Framework, and therefore need to access my DbContext
to get data.
To do this, we should inject the DbContext
, in this case a SampleMessageDbContext
into the constructor of the SampleFactHandler
, then use it locally in the HandleAsync()
method to get a single message.
SampleFactHandler.cs
public class SampleFactHandler : GenericHandler, IHandler { private readonly SampleMessageDbContext _context; public SampleFactHandler(SampleMessageDbContext context) { _context = context; } public override string IntentName => "SampleMessageIntent"; public override Type RequestType => typeof(IntentRequest); public override async Task<SkillResponse> HandleAsync(SkillRequest request) { // just grab one as an example var message = await _context.SampleMessages.FirstOrDefaultAsync(); return ResponseBuilder.Tell(message?.Content ?? "I don't have any messages for you."); } }
Although this simple example just grabs the first row from the db for the SampleMessages
table, you could imagine some more complex data logic applied here to find the proper response we want for the given request. I also added some quick null handling just to clean it up for a real sample.
Registering Handlers with Dependencies
Now that we have our SampleFactHandler
, we need to register it to our ICollection
, but also construct it using the SampleMessageDbContext
. There are a couple ways to do this. For simple situations, you just need to register the DbContext
in your Startup
, then grab it from the ServiceCollection
when creating the List
:
Startup.cs
public class Startup { public Startup(IConfiguration configuration) { Configuration = configuration; } public IConfiguration Configuration { get; } // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { // add other dependencies first services.AddDbContext<SampleMessageDbContext>(options => { options.UseInMemoryDatabase("InMemoryDbForTesting"); }); // Register your handlers here! services.AddScoped<ICollection<IHandler>>(s => { // get the db context to inject in the handlers that are dependent var dbContext = s.GetRequiredService<SampleMessageDbContext>(); return new List<IHandler> { new SimpleLaunchHandler(), new DogFactHandler(), new SampleFactHandler(dbContext) }; }); services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IHostingEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } else { app.UseHsts(); } app.UseHttpsRedirection(); app.UseMvc(); } }
Alternatively, you could create a class whose responsibility is to hold onto the handlers and construct that list with all the IHandlers
. Something like this:
IHandlerContainer.cs
public interface IHandlerContainer { ICollection<IHandler> Handlers { get; } }
HandlerContainer.cs
public class HandlerContainer : IHandlerContainer { public ICollection<IHandler> Handlers { get; private set; } public HandlerContainer(SimpleLaunchHandler launch, DogFactHandler dogFact, SampleFactHandler sampleFact) { Handlers = new List<IHandler> { launch, dogFact, sampleFact } } }
With this container, we update our Startup
to look something like this:
public void ConfigureServices(IServiceCollection services) { // add other dependencies first services.AddDbContext<SampleMessageDbContext>(options => { options.UseInMemoryDatabase("InMemoryDbForTesting"); }); services.AddScoped<SimpleLaunchHandler>(); services.AddScoped<DogFactHandler>(); services.AddScoped<SampleFactHandler>(); services.AddScoped<IHandlerContainer, HandlerContainer>(); services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1); }
Then our Controller
would consume the IHandlerContainer
rather than a flat ICollection
to look like this:
SimpleAlexaController.cs
[Route("[controller]")] public class SimpleAlexaController : Controller { private readonly IHandlerContainer _handlerContainer; public SimpleAlexaController(IHandlerContainer handlerContainer) { _handlerContainer = handlerContainer; } [HttpPost] public async Task<SkillResponse> HandleRequest([FromBody]SkillRequest request) { var viableHandler = _handlerContainer.Handlers.FirstOrDefault(h => h.CanHandle(request)); return await viableHandler.HandleAsync(request); } }
I think this little pattern helps cleanup the Startup
without needing to explicitly pull dependencies out of the ServiceCollection
in order to build the IHandlers
. Which do you prefer?
What’s next?
In future posts, we’ll take a look at building on these types of handlers with things like:
- Well written Unit Tests
- Full Integration Tests
- Advanced Contextual driven handlers
If there’s enough interest in this pattern and the tools I’m building around it, let me know in GitHub or Twitter and I’ll work on getting them into properly libraries and NuGet packages 🙂
Check out more Alexa Developer Tips here: https://alexdunn.org/tag/alexa/
If you like what you see, don’t forget to follow me on twitter @Suave_Pirate, check out my GitHub, and subscribe to my blog to learn more mobile and AI developer tips and tricks!
Interested in sponsoring developer content? Message @Suave_Pirate on twitter for details.
I’m the Director and Principal Architect over at Voicify. Learn how you can use the Voice Experience Platform to bring your brand into the world of voice on Alexa, Google Assistant, Cortana, chat bots, and more: https://voicify.com/