🐙

C#でオブジェクト永続性モデルを使用してDynamoDBにアクセスする方法

2022/01/02に公開

AWSのDVA認定の勉強中に、何となくDynamoDBを扱ってみたくなったので実際に触ってみた。
開発環境と本番環境のアクセスの切替え方についても知りたかったので、そのあたりも調べて実装してみた。

概要

API Gateway + lambda + DynamoDBの典型的な構成で構築。
Lambdaは.Net Core3.1(C#)にて実装。(Visual Studio 2019 communityにて開発。)
API Gatewayの設定はそんなに難しくなかったので手順は省略。

Lambda関数の開発環境構築

  1. Visual Studio 2019 community をインストール
    ※開発している最中はAWS ToolKitがVisual studio 2022に対応していなかっため、2019をインストールした。
  2. AWS Toolkit for Visual Studioをインストール

DynamoDBのテーブル作成

DynamoDBのテーブルを作成。
以下のように、本番環境・開発環境を想定したテーブルを作成。

  • テーブル名
    ※開発環境のテーブルの頭に"dev_"を付ける。理由は後述。
    (必ず先頭に何らかの開発環境のテーブルを示すをプレフィックスを付ける。)

    • 本番環境:UserActionHistory
    • 開発環境:dev_UserActionHistory
  • 項目

    • パーティションキー:UserActionHistory(String)
    • ソートキー:Timestamp(String)

Lambdaの開発

  1. Visual studio 2019で適当なソリューションを作成して、新規プロジェクトを作成。
    プロジェクト作成時は以下を選択。

  2. nugetでAWSSDK.DynamoDBv2をインストール
  3. DynamoDBとマッピングするクラス(UserActionHistoryEntity)を作成
    キー項目の2項目以外は適当な項目を作成。
UserActionHistoryEntity.cs
using System.Collections.Generic;
using Amazon.DynamoDBv2.DataModel;
using System.Text.Json.Serialization;

namespace Entity
{
   [DynamoDBTable("UserActionHistory")]
   public class UserActionHistoryEntity
   {
       /// <summary>ユーザーID(パーティションキー)</summary>
       [JsonPropertyName("UserID")]
       [DynamoDBHashKey]
       public string UserID { get; set; }

       /// <summary>タイムスタンプ(ソートキー)</summary>
       [JsonPropertyName("Timestamp")]
       [DynamoDBRangeKey]
       public string Timestamp { get; set; }

       /// <summary>操作タイプ</summary>
       [JsonPropertyName("Action")]
       [DynamoDBProperty("ActionType")]
       public string Action { get; set; }

       /// <summary>パラメータ</summary>
       /// <remarks>Listとしてマッピング</remarks>
       [JsonPropertyName("Parameters")]
       public List<string> Parameters { get; set; }

   }
}
  1. lambdaのHandlerのコードを書く(とりあえず取得だけ)

ステージ関連の処理はstaticなクラスに纏める。

Stage.cs
using Amazon.Lambda.Core;

namespace AWSLambdaDynamoDBCommon
{
   public static class Stage
   {
       public enum Stages { dev, prod }
       public static Stages CurrentStage { get; private set;}

       public static void SetStage(string lamdaArn)
       {
           if (lamdaArn == null)
           {
               CurrentStage = Stages.dev;
               return;
           }

           foreach(var alias in lamdaArn.Split(":"))
           {
               LambdaLogger.Log(alias);
               if(alias.Trim().ToLower().Equals("prod"))
               {
                   CurrentStage = Stages.prod;
                   return;
               }
           }
           CurrentStage = Stages.dev;
       }
   }
}

実際のLambda関数の処理

AWSLambdaDynamoDBGetFunction.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Text.Json.Serialization;

using Amazon.Lambda.Core;
using Amazon.DynamoDBv2;
using Amazon.DynamoDBv2.DataModel;

using Entity;
using AWSLambdaDynamoDBCommon;

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

namespace AWSLambdaDynamoDBGet
{

   public class AWSLambdaDynamoDBGetFunction
   {
       private static readonly AmazonDynamoDBClient _AmazonDynamoDBClient = new AmazonDynamoDBClient();

       /// <summary>
       /// A simple function that takes a string and does a ToUpper
       /// </summary>
       /// <param name="searchCondition"></param>
       /// <param name="context"></param>
       /// <returns></returns>
       public async Task<List<UserActionHistoryEntity>> FunctionHandler(AWSLambdaDynamoDBGetParamsList searchCondition, ILambdaContext context)
       {
           Stage.SetStage(context.InvokedFunctionArn);
           var setting = new DynamoDBContextConfig();
           setting.TableNamePrefix = Stage.CurrentStage == Stage.Stages.dev ? "dev_" : string.Empty;
           // DynamoDBContextを作成
           using (var dbContext = new DynamoDBContext(_AmazonDynamoDBClient, setting))
           {
               if (searchCondition == null || searchCondition.Count == 0) return null;

               // 単一の場合は通常通り取得
               if (searchCondition.Count <= 1)
               {
                   return new List<UserActionHistoryEntity>() { await dbContext.LoadAsync<UserActionHistoryEntity>(searchCondition[0].UserID, searchCondition[0].Timestamp) };

               }

               // 複数条件の場合はBatchGet
               var batchGet = _CreateBatchGet(searchCondition, dbContext);
               await batchGet.ExecuteAsync();
               return batchGet.Results;

           }
       }

       private BatchGet<UserActionHistoryEntity> _CreateBatchGet(AWSLambdaDynamoDBGetParamsList searchCondition, DynamoDBContext dBContext)
       {
           if (searchCondition == null) return null;

           var getBatch = dBContext.CreateBatchGet<UserActionHistoryEntity>();
           
           foreach( var content in searchCondition.Params)
           {
               getBatch.AddKey(content.UserID, content.Timestamp);
           }

           return getBatch;
           
       }

       public class AWSLambdaDynamoDBGetParamsList
       {
           public AWSLambdaDynamoDBGetParams this[int index]
           {
               get { return Params[index]; }
           }

           public int Count
           {
               get { return Params?.Count ?? 0; }
           }

           [JsonPropertyName("Params")]
           public List<AWSLambdaDynamoDBGetParams> Params { get; set; }
       }

       public class AWSLambdaDynamoDBGetParams
       {
           /// <summary>ユーザーID(パーティションキー)</summary>
           [JsonPropertyName("UserID")]
           public string UserID { get; set; }

           /// <summary>タイムスタンプ(ソートキー)</summary>
           [JsonPropertyName("Timestamp")]
           public string Timestamp { get; set; }
       }
   }
}

Lambda関数の上位にAPI Gatewayを配置してそこから呼び出す想定なので、開発環境、本番環境のアクセス切り替えはAPI Gatewayのステージ変数を使用する。
そのため、Lambda関数内ではLambdaのArn値の末尾によって判定する。

また、テーブル作成時に
・本番環境:UserActionHistor
・開発環境:dev_UserActionHistory
とテーブル名を本番・開発環境で分けているが、AttributeでDynamoDBのテーブルと.netのクラスをマッピングしているため、実際のDynamoDBアクセス時に動的に変更できない。
そのため、開発環境にアクセスする場合、DynamoDBContextConfig.TableNamePrefixに"dev_"と設定する必要がある。
(探し方が悪かったのか、検索してもほとんど情報が出てこなかった・・・Prefixしかプロパティがないので先頭の文字で開発環境のテーブルを区別するしかなさそう。)

  1. lambdaをアップロード
    アップロードをするプロジェクトを右クリックして、Publish to AWS Lambdaを選択してアップロード

  2. テスト実行
    AWS ExplorerからAWS Lambda、作ったLambda関数を右クリックして、View Functionを選択する。
    Jsonのパラメータを記述してInvokeボタンを押せば実行される。

  3. エイリアスの設定
    AWSコンソールのLambdaから作成したLambda関数を選択する。
    アクション→新しいバージョンを作成で、新しいバージョンを作成した後、再度、アクション→エイリアスを作成で下記のエイリアスを2つ作成。

名前 バージョン 説明
dev $LATEST 開発環境
prod 1 本番環境

この後、API Gatewayのステージ変数を設定して、このlambda関数を呼び出すように設定した。
API Gatewayの設定はあまり詰まることはなかったので省略。

上記はGetだけだったが、dbContext.LoadAsyncの部分をSaveAsyncやDeleteAsyncにすると登録・更新、削除ができる。

Discussion