C#でAlexaスキル公式チュートリアル第4回を実装する。

#はじめに
同一セッション内でデータを保持する方法を学んだ公式チュートリアル第3回でしたが、そこから第4回に至るまでは思いの外長い道のりでした。

C#からDynamoDBにアクセスする方法を基礎から調べ、やっとできた、と思ったら低レベルAPIを使っていることがわかり、高レベルAPIでのやり方に変え、最終的にはAlexaスキルでの用途に限定したクラスを作ってそれをNuGetギャラリーに[公開](https://www.nuget.org/packages/AlexaPersistentAttributesManager)、と。

やっと、[公式チュートリアル第4回](https://developer.amazon.com/ja/blogs/alexa/post/4144a8ea-7549-4c44-a4bd-e94cb93807ea/chapter4-jp)をC#で実装する準備が整いました。

#NuGetギャラリーからパッケージをインストール
Visual StudioでNuGetパッケージマネージャーを使って`AlexaPersistentAttributesManager`をインストールしてください。

#ざっくり使い方
Alexaスキル特化ですが、DynamoDBにデータを追加したり、データを取得したりするのがほんの数ステップでできます。

## `AttributesManager`クラスをインスタンス化

```csharp
//このとき、コンストラクタの中で指定したテーブルに`_tableName`で指定したテーブルが作られます。`userId`はgetオンリーなプロパティに格納され、テーブルへのデータの追加、取得の際にプライマリキーとして使われます。
var attrManager = new AttributesManager(userId, _tableName);
```

## DynamoDB上のテーブルにデータをセットする方法
attrManagerにキーバリューペアの形でデータを追加していく。

```csharp
//"starSign" as a key, "Gemini" as a value
attrManager.SetPersistentAttributes("starSign", "Gemini");
attrManager.SetPersistentAttributes("color", "blue");
//.....
```

`SavePersistentAttributes`メソッドでデータをDynamoDB内のテーブルに追加します。

```csharp
attrManager.SavePersistentAttributes();
```


## DynamoDB上のテーブルからデータを取得する方法
`GetPersistentAttributes`メソッドでテーブル上の`id`カラムが`userId`であるレコードの`attributes`カラムの値を取得します。
取得したデータはキーバリューペアなのでキーを指定すると、対応する値を取得することができます。

```csharp
var attr = attrManager.GetPersistentAttributes();
var sign = attr["starSign"];
```


#こちらがソースコード全体になります
`HoroscopeIntentHandler`内で「星座」の情報を取得して、それをセッションアトリビュートと永続アトリビュート（DynamoDB）に格納します。

そして`LuckyColorIntentHandler`ではセッションアトリビュートまたは永続アトリビュートを取得しています。


```csharp:Function.cs
using Alexa.NET.Request;
using Alexa.NET.Request.Type;
using Alexa.NET.Response;
using Amazon.DynamoDBv2;
using Amazon.DynamoDBv2.Model;
using Amazon.Lambda.Core;
using Amazon.Runtime.Internal;
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using AlexaPersistentAttributesManager;
using Amazon;
using Amazon.Runtime;

// Assembly attribute to enable the Lambda function's JSON input to be converted into a .NET class.
[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.Json.JsonSerializer))]

//公式チュートリアル第4回（https://developer.amazon.com/ja/blogs/alexa/post/4144a8ea-7549-4c44-a4bd-e94cb93807ea/chapter4-jp）の内容です。
namespace HoroscopeSkill_CSharp
{
    public class Function
    {

        private class FortuneScore
        {
            public string Score { get; }
            public string Description { get; }

            public FortuneScore(string score, string description)
            {
                this.Score = score;
                this.Description = description;
            }
        }

        private readonly List<FortuneScore> _fortunes = new List<FortuneScore>
        {
            new FortuneScore("good","星みっつで良いでしょう"),
            new FortuneScore("normal","星ふたつで普通でしょう"),
            new FortuneScore("bad","星ひとつでイマイチでしょう"),
        };

        private readonly string[] _luckyColors =
        {
            "赤",
            "ピンク",
            "オレンジ",
            "ブルー",
            "水色",
            "紺色",
            "紫",
            "黒",
            "グリーン",
            "レモンイエロー",
            "ホワイト",
            "チャコールグレー"
        };


        private readonly string _tableName = "HoroscopeSkillTableCSharp";

        /// <summary>
        /// A simple function that takes a string and does a ToUpper
        /// </summary>
        /// <param name="skillRequest"></param>
        /// <param name="context"></param>
        /// <returns></returns>
        public SkillResponse FunctionHandler(SkillRequest skillRequest, ILambdaContext context)
        {
            SkillResponse skillResponse = null;

            try
            {
                //型スイッチの利用
                switch (skillRequest.Request)
                {
                    case LaunchRequest launchRequest:
                        skillResponse = HelpIntentHandler(skillRequest);
                        break;
                    case IntentRequest intentRequest:
                        switch (intentRequest.Intent.Name)
                        {
                            case "HoroscopeIntent":
                                skillResponse = HoroscopeIntentHandler(skillRequest);
                                break;
                            case "LuckyColorIntent":
                                skillResponse = LuckyColorIntentHandler(skillRequest);
                                break;
                            case "AMAZON.HelpIntent":
                                skillResponse = HelpIntentHandler(skillRequest);
                                break;
                            case "AMAZON.CancelIntent":
                                skillResponse = CancelAndStopIntentHandler(skillRequest);
                                break;
                            case "AMAZON.StopIntent":
                                skillResponse = CancelAndStopIntentHandler(skillRequest);
                                break;
                            default:
                                //skillResponse = ErrorHandler(skillRequest);
                                break;
                        }

                        break;
                    case SessionEndedRequest sessionEndedRequest:
                        skillResponse = SessionEndedRequestHandler(skillRequest);
                        break;
                    default:
                        //skillResponse = ErrorHandler(skillRequest);
                        break;
                }
            }
            catch
            {
                skillResponse = ErrorHandler(skillRequest);
            }

            return skillResponse;
        }




        #region 各インテント、リクエストに対応する処理を担当するメソッドたち

        private SkillResponse HoroscopeIntentHandler(SkillRequest skillRequest)
        {
            var intentRequest = skillRequest.Request as IntentRequest;

            var speechText = "";

            var skillResponse = new SkillResponse
            {
                Version = "1.0",
                Response = new ResponseBody()
            };

            //StarSignスロットから値を取り出します。
            var sign = intentRequest.Intent.Slots["StarSign"].Value;

            //占い結果をランダムに取り出す
            var random = new Random();
            int fortuneIdx = random.Next(3);
            var fortune = _fortunes[fortuneIdx];

            speechText = $"今日の{sign}の運勢は{fortune.Description}。";
            var repromptText = "他にラッキーカラーが占えます。ラッキーカラーを聞きますか？";

            skillResponse.Response.OutputSpeech = new PlainTextOutputSpeech
            {
                Text = speechText + repromptText
            };
            skillResponse.Response.Card = new SimpleCard
            {
                Title = "サンプル星占い",
                Content = speechText
            };
            skillResponse.Response.Reprompt = new Reprompt
            {
                OutputSpeech = new PlainTextOutputSpeech
                {
                    Text = repromptText
                }
            };



            #region セッションオブジェクトを利用
            //セッションオブジェクトを取得
            var attributes = skillResponse.SessionAttributes;

            //nullだったらインスタンスを生成
            if (attributes == null)
            {
                attributes = new Dictionary<string, object>();
            }
            //「sign」をキーにしてユーザーの星座を格納
            attributes["sign"] = sign;
            //レスポンスに格納
            skillResponse.SessionAttributes = attributes;

            #endregion



            #region DynamoDBを利用した永続アトリビュート

            //レコードのプライマリキーにuserIdを使用
            var userId = skillRequest.Session.User.UserId;

            var attrManager=new AttributesManager(userId,_tableName);
            

            //ユーザーの星座情報をsignをキーにしてセット
            attrManager.SetPersistentAttributes("sign", sign);
            //セットした情報をDynamoDBに保存
            attrManager.SavePersistentAttributes();

            #endregion


            return skillResponse;
        }



        private SkillResponse LuckyColorIntentHandler(SkillRequest skillRequest)
        {
            var intentRequest = skillRequest.Request as IntentRequest;

            var speechText = "";

            var skillResponse = new SkillResponse
            {
                Version = "1.0",
                Response = new ResponseBody()
            };


            //ローカル関数だった。忘れてた。
            //
            SkillResponse ComposeReturnToAskFortuneResponse()
            {
                speechText = "そういえばまだ運勢を占っていませんでしたね。";
                speechText += "今日の運勢を占います。" +
                              "たとえば、ふたご座の運勢を教えてと聞いてください";

                skillResponse.Response.OutputSpeech = new PlainTextOutputSpeech
                {
                    Text = speechText
                };
                skillResponse.Response.Reprompt = new Reprompt
                {
                    OutputSpeech = new PlainTextOutputSpeech
                    {
                        Text = speechText
                    }
                };

                return skillResponse;
            }


            #region セッションアトリビュートから値を取得
            var sign = skillRequest.Session.Attributes?["sign"].ToString() ?? "";

            #endregion



            #region DynamoDBから値を取得

            if (string.IsNullOrEmpty(sign))
            {
                var userId = skillRequest.Session.User.UserId;
                var attrManager = new AttributesManager(userId, _tableName);

                var attr = attrManager.GetPersistentAttributes();

                sign = attr?["sign"] ?? "";
            }

            #endregion


            //セッションアトリビュートと永続アトリビュートのどちらにも値が入っていなければ
            //ここでリターン
            if (string.IsNullOrEmpty(sign))
            {
                return ComposeReturnToAskFortuneResponse();
            }


            //ラッキーカラーをランダムで。
            var random = new Random();
            var luckyColorIdx = random.Next(3);
            var luckyColor = _luckyColors[luckyColorIdx];

            speechText = $"今日の{sign}のラッキーカラーは" +
                         $"{luckyColor}です。素敵ないちにちを。";

            skillResponse.Response.OutputSpeech = new PlainTextOutputSpeech
            {
                Text = speechText
            };
            skillResponse.Response.Card = new SimpleCard
            {
                Title = "サンプル星占い",
                Content = speechText
            };
            skillResponse.Response.ShouldEndSession = true;//セッション終了を指定

            return skillResponse;
        }

        private SkillResponse HelpIntentHandler(SkillRequest skillRequest)
        {
            var intentRequest = skillRequest.Request as IntentRequest;

            var speechText = "今日の運勢を占います。" +
                "例えば、ふたご座の運勢を教えてと聞いてください。";

            var skillResponse = new SkillResponse
            {
                Version = "1.0",
                Response = new ResponseBody()
            };

            skillResponse.Response.OutputSpeech = new PlainTextOutputSpeech
            {
                Text = speechText
            };
            skillResponse.Response.Reprompt = new Reprompt
            {
                OutputSpeech = new PlainTextOutputSpeech
                {
                    Text = speechText
                }
            };

            return skillResponse;
        }


        private SkillResponse CancelAndStopIntentHandler(SkillRequest skillRequest)
        {
            var intentRequest = skillRequest.Request as IntentRequest;

            var speechText = "";

            var skillResponse = new SkillResponse
            {
                Version = "1.0",
                Response = new ResponseBody()
            };

            skillResponse.Response.OutputSpeech = new PlainTextOutputSpeech
            {
                Text = speechText
            };
            skillResponse.Response.Card = new SimpleCard
            {
                Title = "サンプル星占い",
                Content = speechText
            };
            skillResponse.Response.ShouldEndSession = true;

            return skillResponse;
        }


        private SkillResponse SessionEndedRequestHandler(SkillRequest skillRequest)
        {
            var sessionEndedRequest = skillRequest.Request as SessionEndedRequest;

            return new SkillResponse
            {
                Version = "1.0",
                Response = new ResponseBody()
            };
        }


        private SkillResponse ErrorHandler(SkillRequest skillRequest)
        {
            var speechText = "すみません。聞き取れませんでした。";

            var skillResponse = new SkillResponse
            {
                Version = "1.0",
                Response = new ResponseBody()
            };

            skillResponse.Response.OutputSpeech = new PlainTextOutputSpeech
            {
                Text = speechText
            };
            skillResponse.Response.Reprompt = new Reprompt
            {
                OutputSpeech = new PlainTextOutputSpeech
                {
                    Text = speechText
                }
            };

            return skillResponse;
        }

        #endregion
    }
}

```

#試してみます
公式チュートリアル第4回と同じ手順で試してみましょう。
<img src="https://qiita-image-store.s3.amazonaws.com/0/245438/542ffbef-1594-f7fd-050f-e7c7b5b53f01.png" title="" alt="image.png" width="60%" height="" >

同じ結果が返ってきています。
セッションが一度終了した後、直接ラッキーカラーを訊くと、前回DynamoDBに格納した星座情報を取得して使ってくれています。
ちゃんと動いているようですね。