C# で Azure Cosmos DB のデータを取得して LINE に通知してみた

Azure Cosmos DB のコンテナに記録されている NoSQL なデータを抽出するためのメモです。

今回の内容は Minecraft の農業自動化の続きみたいな感じです。

blog.takunology.jp

上の記事では Azure Functions を使用して Azure 上で完結していますが、今回は Functions ではなくローカルのコンソールアプリから Cosmos DB に接続してデータだけを取ってくる作業になります。もし、Azure 上で完結したい場合は今回のソースコードを Functions に移植すればできると思います。(時間作ってやってみたいと思います。)

目標

  • C# で Azure Cosmos DB のコンテナにあるアイテム(プロパティ)を取得して表示する。
  • 取得したデータを使いやすい形にして応用する。

1. Azure Cosmos DB の準備

まずは Azure 上で Cosmos DB のアカウントとリソースを作成してデプロイします。私は以前作成した Minecraft の作物収穫データを使用します。リソースの作成方法については公式に詳しく書かれているので参照してください。

docs.microsoft.com

私の環境では HarvestContainer というコンテナにアイテムが3つ登録されています。それぞれプロパティはこのようになっています。

f:id:takunology:20210307142158p:plain

  • Date : 収穫日
  • Time : 収穫した時間
  • WheatCount : 収穫した小麦の数
  • PotatoCount : 収穫したジャガイモの数
  • CarrotCount : 収穫したニンジンの数
  • id : ユーザが定義した ID (今回は自動生成)
  • _rid : コンテナのユニーク識別 ID
  • _self : コンテナのアドレス指定可能な URI
  • _etag : オプティミスティック同時実行制御に使用するタグ
  • _attachments : 添付ファイルリソースのアドレス指定可能なパス
  • _ts : コンテナの最終更新タイムスタンプ

id まではユーザが定義でき、そこから下はシステムによって自動生成されます。オプティミスティック同時実行制御は、複数のユーザで同じデータを参照や更新するときの競合問題を制御するための機能らしいです。

2. プログラムを書く

今回は .NET Core (コンソールアプリケーション) で作成します。Azure Cosmos DB を操作するためには2つのパッケージが必要です。

install-package Microsoft.Azure.DocumentDB
install-package Microsoft.Azure.DocumentDB.Core

導入できたらプログラムを書いていきますが、その前にデータベースにアクセスするためのキーが必要なので、Azure ポータル上で確認しておきます。Cosmos DB アカウントリソースの左側に キー という項目があるので選択します。

f:id:takunology:20210307144346p:plain

ここの URIプライマリキー を使用します。また、作成したデータベース名とコンテナ名も確認します。これらの情報をもとに、Cosmos DB のデータを取得するプログラムを書きます。

using Microsoft.Azure.Documents;
using Microsoft.Azure.Documents.Client;
using System;
using System.Threading.Tasks;

//namespace
class Program
{
    private static readonly string uri = @"Cosmos DB URI";
    private static readonly string key = @"Cosmos DB primary key";
    private static readonly string databaseName = "Database Name";
    private static readonly string collectionName = "Database Container (Collection) Name";

    static async Task Main(string[] args)
    {
        var client = new DocumentClient(new Uri(uri), key);
        var uriFactory = UriFactory.CreateDocumentCollectionUri(databaseName, collectionName);
        var feedOptions = new FeedOptions { MaxItemCount = -1 };

        try
        {
            var feed = await client.ReadDocumentFeedAsync(uriFactory, feedOptions);

            foreach (Document document in feed)
            {
                Console.WriteLine(document);
            }
        }
        catch (Exception e)
        {
            Console.WriteLine(e);
        }
    }
}

3. 実行結果

このコードをそのまま実行するとこのようになります。

f:id:takunology:20210307151447p:plain

先ほど載せた画像と、コンソールに出力された3番目のオブジェクトと見比べてみると、同じデータが取得できています。

これで目標であるアイテムの取得を行うことができました。ここで終わってもいいのですが、せっかくなのでデシリアライズして収穫データだけを抽出してみたいと思います。

4. デシリアライズ機能の追加

毎度おなじみの Jsonシリアライズを行います。このブログでは何度も紹介しているので、解説は特にしません。

using System.Runtime.Serialization;

[DataContract]
public class DocumentsData
{
    [DataMember(Name = "Date")]
    public string Date { get; set; }

    [DataMember(Name = "Time")]
    public string Time { get; set; }

    [DataMember(Name = "WheatCount")]
    public int WheatCount { get; set; }

    [DataMember(Name = "PotatoCount")]
    public int PotatoCount { get; set; }

    [DataMember(Name = "CarrotCount")]
    public int CarrotCount { get; set; }

    [DataMember(Name = "id")]
    public string Id { get; set; }

    [DataMember(Name = "_rid")]
    public string Rid { get; set; }

    [DataMember(Name = "_self")]
    public string Self { get; set; }

    [DataMember(Name = "_etag")]
    public string Etag { get; set; }

    [DataMember(Name = "_attachments")]
    public string Attachments { get; set; }

    [DataMember(Name = "_ts")]
    public int Ts { get; set; }
}

あとはソースコードを変更していきます。 Main メソッドとデータ取得用メソッドで分けます。

using System.IO;
using System.Net;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Json;
using System.Text;

static void Main(string[] args)
{
    var items = Task.Run(async () => {
        return await GetHarvestDatas();
    }).GetAwaiter().GetResult();

    foreach (var item in items)
    {
        Console.Write(
            "収穫日 : " + item.Date + " " + item.Time + "\n" +
            "収穫した小麦 : " + item.WheatCount + "\n" +
            "収穫したジャガイモ : " + item.PotatoCount + "\n" +
            "収穫したニンジン : " + item.CarrotCount + "\n\n");
    }
}
static async Task<List<HarvestData>> GetHarvestDatas()
{
    var client = new DocumentClient(new Uri(uri), key);
    var uriFactory = UriFactory.CreateDocumentCollectionUri(databaseName, collectionName);
    var feedOptions = new FeedOptions { MaxItemCount = -1 };

    var harvestDatas = new List<HarvestData>();

    try
    {
        var feed = await client.ReadDocumentFeedAsync(uriFactory, feedOptions);

        foreach (Document document in feed)
        {
            var serializer = new DataContractJsonSerializer(typeof(HarvestData));
            var ms = new MemoryStream(Encoding.UTF8.GetBytes(document.ToString()));
            var harvestdata = serializer.ReadObject(ms) as HarvestData;

            harvestDatas.Add(harvestdata);
        }
        return harvestDatas;
    }
    catch (Exception e)
    {
        Console.WriteLine(e);
        return null;
    }
}

これで完成です。

5. 実行結果その2

これで Cosmos DB に記録された収穫データを見やすくできました。

f:id:takunology:20210307155828p:plain

めでたし、めでたし。

と思ったのですが、もう少し応用していきます。

6. LINE で通知する

せっかく収穫量を取得できたので、LINE に送信してみます。ソースコードは以前書いたものを使いまわします。アカウント登録とか諸々のやり方はこの記事を参考にどうぞ。ここでは Messaging API を使用できる環境が整っているとして進めます。

blog.takunology.jp

Webhook 用にシリアライズして Messaging API を動かします。(使いまわし)

[DataContract]
public class LineMessage
{
    [DataMember(Name = "messages")]
    public IList<Message> Message { get; set; } = new List<Message>();
}

[DataContract]
public class Message
{
    [DataMember(Name = "type")]
    public string Type { get; set; }
    [DataMember(Name = "text")]
    public string Text { get; set; }
}

メッセージ送信用のメソッドを用意して、Main から呼び出します。(使いまわし)

public static string token = "アクセストークン";
public static string url = @"https://api.line.me/v2/bot/message/broadcast";

static void Main(string[] args)
{
    var items = Task.Run(async () => {
        return await GetHarvestDatas();
    }).GetAwaiter().GetResult();

    foreach (var item in items)
    {
        SendMessage(item);
    }
}

public static void SendMessage(HarvestData harvestData)
{
    var lineMessage = new LineMessage();

    lineMessage.Message.Add(new Message
    {
        Type = "text",
        Text = "収穫が完了したよ!\n" +
        "収穫日 : " + harvestData.Date + " " + harvestData.Time + "\n" +
        "🌾 : " + harvestData.WheatCount + "\n" +
        "🥔 : " + harvestData.PotatoCount + "\n" +
        "🥕 : " + harvestData.CarrotCount
    });

    var serializer = new DataContractJsonSerializer(typeof(LineMessage));
    var ms = new MemoryStream();
    Serializer.WriteObject(ms, lineMessage);

    using (WebClient client = new WebClient())
    {
        client.Encoding = Encoding.UTF8;
        client.Headers.Add("Content-Type", "application/json");
        client.Headers.Add("Authorization", "Bearer " + token);
        client.UploadString(url, Encoding.UTF8.GetString(ms.ToArray()));
    }
}

ちなみに、絵文字も使用できるので使ってみました。

7. 実行結果その3 (LINE)

送信されたメッセージは Cosmos DB に記録されているアイテム数だけ表示されます。

f:id:takunology:20210307165002j:plain

f:id:takunology:20210307165016j:plain

このままだとデータが蓄積されるほど大量のメッセージが送られてくるので、収穫が完了したらリストの最後の要素を取り出して送信するように変更すれば1件ずつ送られてくるようになります。

おわりに

今回は Azure Cosmos DB で取得した Minecraft の作物収穫量を LINE に送信するものを作ってみました。プログラムの実行はローカル環境でしたが、これを Functions に移植すれば全て Azure がやってくれるので究極の自動化になりそうです。

Minecraft はゲームと言えばゲームなのですが、このようなことにも利用できるので学習コンテンツとしても期待できそうです。また、Minecraft だけではなく実際の農業にも応用できると思います。IoT 機器と連動して家庭菜園(水やりとか収穫時期のお知らせ)の自動化?みたいなシステムとか面白そうですね。

ソースコード

今回作ったソースコードです。参考にどうぞ。

github.com

参考

参考になったサイトです。ありがとうございます。

docs.microsoft.com

docs.microsoft.com

docs.microsoft.com