Azure Functions 3.x で .NET 5.0 を使って Dataverse に格納されているレコードを取得する

Power Apps と共に使われることが多く、また、 Dynamics 365 Customer Engagement 製品群の既定のデータストアとして機能するのが Dataverse です。.NET の技術を使って Dataverse の API を呼び出そうとすると、これまで長らく .NET Framework v4.x が必要でした。  
先日、 NuGet パッケージ Microsoft.PowerPlatform.Dataverse.Client (執筆時点で Public preview) を使うと、 .NET 5.0 がサポートされていることを発見しました。  
それを使ってみたくって、この記事では、これまた最近 .NET 5 がサポートされた Azure Functions アプリの中から利用してみます。

# 目的
以下のようなアプリを開発します。

- Azure Functions アプリである
    - Azure Functions v3
    - .NET 5.0 の C# で開発する
    - Visual Studio Code を使う
    - 接続文字列をコードの中に記述せず、Azure ポータル上で登録する
    - Dataverse にアクセスして、エンティティ レコードの プライマリ画像 のデータを返す
        - NuGet パッケージ Microsoft.PowerPlatform.Dataverse.Client v0.4.12 (Public preview) を利用する

# 開発環境
- .NET SDK 5.0.301
- .NET Core SDK 3.1.410
    - (Azure Functions v3 で .NET 5 を採用する際にも .NET Core 3.1 が必要なようです。詳しく調べてません。)
- Visual Studio Code v1.57.0

# 開発手順
### ① アプリの登録などの認証の設定
後に Azure Functions アプリ内から Dataverse にアクセスする際、 OAuth 認証が使用されます。そのために、以下のページの記述にしたがって、アプリを登録したり Dataverse へのアクセス権を付与する必要があります。「アプリ登録」セクションにおいては、アプリケーションの種類としては "Web アプリ /API" の方が該当します。  

- Microsoft Dataverse で OAuth 認証を使用する ( https://docs.microsoft.com/ja-jp/powerapps/developer/data-platform/authenticate-oauth )

### ② Azure Functions アプリの開発
##### 1. Azure ポータル上で Azure Functions の 関数アプリ を作成します  
関数アプリ を新規作成します。以下のように設定しました。

- ランタイムスタック: .NET
- Version: 3.1 (他の選択肢がない)

##### 2. Visual Studio Code 上での 関数アプリの関数の開発を開始します
以下のページを参考に開発の準備をします。

- Visual Studio Code を使用して Azure Functions を開発する ( https://docs.microsoft.com/ja-jp/azure/azure-functions/functions-develop-vs-code?tabs=csharp )

##### 3. Visual Studio Code 内の Azure Functions 拡張機能を使ってプロジェクトを作成する
私は作成時に、以下の選択肢を選びました。 

- 言語: C# (これは、上記ページに記載の "C# コンパイル済み" に該当するはず)
- .NET ランタイム: .NET 5
- テンプレート: HTTP trigger
- 関数名: HttpTrigger1
- 名前空間: Keiji.Function
- AccessRIghts (認証レベルのことであろう): Function

##### 4. 接続文字列を登録する
関数アプリから Dataverse にアクセスするための、接続文字列を登録します。Azure ポータル上で当該 関数アプリ についての Configuration で、新しい Connection string を登録します。
![ConnectionString.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/455051/6f040057-3679-ce87-27cb-db67c61dff21.png)
登録する Connection string として、私は名前を "MyCDSServer" とし、値は、以下のようなもので登録しました。「① アプリの登録などの認証の設定」で登録したアプリにしたがって、適宜ご自身の環境に合わせて編集ください。

```:MyCDSServer&#x20;の値
AuthType=OAuth;Username=user@xxxxxxx.onmicrosoft.com;Password=12345;Url=https://xxxxxxx.crm.dynamics.com;AppId=xxxxxxxxx-8155-44e5-95e4-8615e5d12150;RedirectUri=https://localhost/appname;LoginPrompt=Auto
```  

この辺りは、以下のページの記載を参考にしています。

- XRM ツールの接続文字列を使用して Microsoft Dataverse に接続する ( https://docs.microsoft.com/ja-jp/powerapps/developer/data-platform/xrm-tooling/use-connection-strings-xrm-tooling-connect )

##### 5. 接続文字列をコードから取得する準備
Visual Studio Code にて、以下のページに記載の通り  Startup.cs ファイルを追加します。namespace 関連以外は同じです。

- App Configuration ストアに接続する ( https://docs.microsoft.com/ja-jp/azure/azure-app-configuration/quickstart-azure-functions-csharp#connect-to-an-app-configuration-store )

```c#:Startup.cs
using System;
using Microsoft.Azure.Functions.Extensions.DependencyInjection;
using Microsoft.Extensions.Configuration;

[assembly: FunctionsStartup(typeof(Keiji.Function.Startup))]

namespace Keiji.Function
{
    class Startup : FunctionsStartup
    {
        public override void ConfigureAppConfiguration(IFunctionsConfigurationBuilder builder)
        {
            string cs = Environment.GetEnvironmentVariable("ConnectionString");
            builder.ConfigurationBuilder.AddAzureAppConfiguration(cs);
        }

        public override void Configure(IFunctionsHostBuilder builder)
        {
        }
    }
}
```

##### 6. 接続文字列をコードから取得する
Visual Studio Code で、関数である HttpTrigger1.cs で、実際に接続文字列を取得します。コンストラクタを追加するので、静的クラスではなく、動的クラスにするため、 static を取り除いています。(コードの全体は後程記載します。)

```c#:HttpTrigger1.cs&#x20;の一部
    public class HttpTrigger1
    {
        private IConfiguration _configuration;
        public HttpTrigger1(IConfiguration configuration)
        {
            _configuration = configuration;
        }
```

##### 7. 関数で受け取るためのクラスを作成する
後に Dataverse に対するクエリのために必要なクラスを作成します。具体的には、以下のような JSON 文字列を受け付けたいという設計です。
この例では、論理名 keiji_property というエンティティがあって、その中で プライマリ画像 データが格納されている属性の論理名が keiji_primaryimage である、という意味合いです。  

```json
{ "entityName": "keiji_property", "recordId": "76485703-2556-eb11-bb23-000d3a8b6647", "imageAttributeName": "keiji_primaryimage" }
```

その JSON 文字列に対応するクラスとして、今回は HttpTrigger1.cs の中に以下のようなクラスを作成しました。

```c#:HttpTrigger1.cs&#x20;の一部
    public class MyClass
    {
        public string entityName { get; set; }
        public string recordId { get; set; }
        public string imageAttributeName { get; set; }
    }
```
##### 8. NuGet パッケージのインストール
NuGet パッケージ Microsoft.PowerPlatform.Dataverse.Client をインストールします。

```:.NET&#x20;CLI
dotnet add package Microsoft.PowerPlatform.Dataverse.Client --version 0.4.12
```

##### 9. Dataverse へのクエリを発行して、レスポンスを返す
ServiceClient が NuGet パッケージ Microsoft.PowerPlatform.Dataverse.Client にて提供されていて、以下のようなコードで利用できます。今回は RetrieveAsync() を使って、単一の エンティティ レコード の 1個の属性の値を取得しようとしています。
ServiceClient を利用する際に、接続文字列を利用しています。

```c#:HttpTrigger1.cs&#x20;の一部
            string connectionString = _configuration.GetConnectionString("MyCDSServer");
            var serviceClient = new ServiceClient(connectionString);

            var result = await serviceClient.RetrieveAsync(
                myClass.entityName,
                new Guid(myClass.recordId),
                new ColumnSet(myClass.imageAttributeName)
            );

            var response = req.CreateResponse(HttpStatusCode.OK);
            response.Headers.Add("Content-Type", "text/plain; charset=utf-8");

            byte[] responseByteArray = (byte[])result[myClass.imageAttributeName];
            string imageBase64 = Convert.ToBase64String(responseByteArray, 0, responseByteArray.Length);
            response.WriteString("data:image/png;base64," + imageBase64);

            return response;
```

##### 10. 関数のコード全体
以下が、関数のコード全体です。

```c#:HttpTrigger1.cs
using System;
using System.Net;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;
using Microsoft.Extensions.Logging;
using System.Threading.Tasks;
using System.Text.Json;
using Microsoft.Extensions.Configuration;
using Microsoft.PowerPlatform.Dataverse.Client;
using Microsoft.Xrm.Sdk.Query;

namespace Keiji.Function
{
    public class HttpTrigger1
    {
        private IConfiguration _configuration;
        public HttpTrigger1(IConfiguration configuration)
        {
            _configuration = configuration;
        }

        [Function("HttpTrigger1")]
        public async Task<HttpResponseData> Run([HttpTrigger(AuthorizationLevel.Function, "get", "post")] HttpRequestData req,
            FunctionContext executionContext)
        {
            var logger = executionContext.GetLogger("HttpTrigger1");
            logger.LogInformation("C# HTTP trigger function processed a request.");

            byte[] byteArray = new byte[req.Body.Length];
            await req.Body.ReadAsync(byteArray, 0, (int)req.Body.Length);
            string jsonContent = System.Text.Encoding.UTF8.GetString(byteArray);
            MyClass myClass = JsonSerializer.Deserialize<MyClass>(jsonContent);

            string connectionString = _configuration.GetConnectionString("MyCDSServer");
            var serviceClient = new ServiceClient(connectionString);

            var result = await serviceClient.RetrieveAsync(
                myClass.entityName,
                new Guid(myClass.recordId),
                new ColumnSet(myClass.imageAttributeName)
            );

            var response = req.CreateResponse(HttpStatusCode.OK);
            response.Headers.Add("Content-Type", "text/plain; charset=utf-8");

            byte[] responseByteArray = (byte[])result[myClass.imageAttributeName];
            string imageBase64 = Convert.ToBase64String(responseByteArray, 0, responseByteArray.Length);
            response.WriteString("data:image/png;base64," + imageBase64);

            return response;
        }
    }

    public class MyClass
    {
        public string entityName { get; set; }
        public string recordId { get; set; }
        public string imageAttributeName { get; set; }
    }
}
```

##### 11. (参考) .csproj ファイル
参考までに、 .csproj ファイルを記載します。

```xml
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net5.0</TargetFramework>
    <AzureFunctionsVersion>v3</AzureFunctionsVersion>
    <OutputType>Exe</OutputType>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Microsoft.Azure.Functions.Extensions" Version="1.1.0" />
    <PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http" Version="3.0.12" />
    <PackageReference Include="Microsoft.Azure.Functions.Worker.Sdk" Version="1.0.3" OutputItemType="Analyzer" />
    <PackageReference Include="Microsoft.Azure.Functions.Worker" Version="1.1.0" />
    <PackageReference Include="Microsoft.Extensions.Configuration.AzureAppConfiguration" Version="4.4.0" />
    <PackageReference Include="Microsoft.PowerPlatform.Dataverse.Client" Version="0.4.12" />
  </ItemGroup>
  <ItemGroup>
    <None Update="host.json">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
    </None>
    <None Update="local.settings.json">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
      <CopyToPublishDirectory>Never</CopyToPublishDirectory>
    </None>
  </ItemGroup>
</Project>
```

### ③ Azure Functions アプリのデプロイ
Visual Studio Code 内の Azure Functions 拡張機能を使って、Azure 上の関数アプリにデプロイします。

### ④ 動作確認
##### 1. 関数を呼び出すための URL を取得
Visual Studio Code 内の Azure Functions 拡張機能を使って、関数の URL を取得 ("Copy Function Url") します。
![CopyFunctionUrl.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/455051/f9f26398-c483-d85e-2ccb-7fb52b387324.png)

その URL は以下のようなものになります。

```
https://keiji-function-app-xxxxxx.azurewebsites.net/api/httptrigger1?code=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
```

##### 2. Postman を使って動作確認
Postman を使って、先ほど得た URL に対して、 POST メソッドを使って、先に紹介した JSON 文字列を Body に入力して Send します。
以下のように画像データを取得できると成功です。
なお、(今回の Dataverse へのクエリの例では) 事前に Dataverse の当該エンティティで当該レコードIDを持つレコードに、画像が登録されていることを確認してください。
![Postman.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/455051/3450c121-602e-cba5-5040-488cc02baa43.png)

# リファレンス

- NuGet パッケージ Microsoft.PowerPlatform.Dataverse.Client ( https://www.nuget.org/packages/Microsoft.PowerPlatform.Dataverse.Client/ )
    - そのプロジェクトサイト ( https://github.com/microsoft/PowerPlatform-DataverseServiceClient )