yotiky Tech Blog

とあるエンジニアの備忘録

Azure Functions INDEX

Azure Functions 備忘録

目次

AuthorizationLevel

説明
Anonymous API キーは必要ありません。
User *EasyAuth で使われる予定らしいが未だサポートされていないらしい
Function 関数固有の API キーが必要です。 何も指定されなかった場合は、これが既定値になります。
System
Admin マスター キーが必要です。

リクエストのサイズ制限

HTTP 要求の長さは 100 MB (104,857,600 バイト) に、URL の長さは 4 KB (4,096 バイト) バイトに制限されています。 これらの制限は、ランタイムの Web.config ファイルの httpRuntime 要素で指定されています。

Windows ローカルで実行した場合は 30MB で制限されてしまう不具合があるとかないとか。

Azure Functions で 複数ファイルをアップロードする

目次

検証環境

サーバーサイド

クライアントからは、multipart/form-data 形式で POST してもらう想定。

[FunctionName("Upload")]
public async Task<IActionResult> Run(
    [HttpTrigger(AuthorizationLevel.Function, "post", Route = null)] HttpRequest req,
    ILogger log)
{
    log.LogInformation("C# HTTP trigger function processed a request.");

    // UserAgent を取得するキーは "User-Agent"
    log.LogInformation(req.Headers["User-Agent"][0]);

    var form = await req.ReadFormAsync();

    foreach (var file in form.Files)
    {
        log.LogInformation(file.FileName);
    }
    foreach (var kvp in form)
    {
        log.LogInformation($"{kvp.Key}({kvp.Value.Count}): {kvp.Value}");
    }

    return new OkObjectResult("OK");
}

クライアント再度

webkitdirectory 属性

webkitdirectory 属性を使用すると、ファイルを選択する代わりにフォルダを選択できるようになる。送信されたファイルのファイル名は相対パス付きのため、ディレクトリのツリー構造の情報も取得できるようになる。

developer.mozilla.org

<form action="http://localhost:7071/api/Upload", method="POST", enctype="multipart/form-data">
    BaseDir: <input type="text" name=projectId/><br/>
    Folder: <input type="file" name="files" webkitdirectory multiple/><br/>
    <input type="submit" value="Submit"/>
    <input type="reset" value="Reset"/>
</form>

f:id:yotiky:20210227015224p:plain

サーバーサイドの実行結果。
ファイル名が選択したフォルダを基点とした相対パスになっている。

[2021-02-26T17:46:19.298Z] C# HTTP trigger function processed a request.
[2021-02-26T17:46:19.320Z] Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36
[2021-02-26T17:46:19.322Z] wallpapers/glanpalace1.jpg
[2021-02-26T17:46:19.322Z] wallpapers/photo1/aurora2.jpg
[2021-02-26T17:46:19.323Z] wallpapers/photo1/icelandlake2540.jpg
[2021-02-26T17:46:19.323Z] basedir/(1): wallpapers
[2021-02-26T17:46:19.325Z] Executed 'Upload' (Succeeded, Id=6a51b942-e294-401c-b344-b3395e53f9a5, Duration=140ms)

f:id:yotiky:20210227024639p:plain

Postman

Postman では form-data のファイル選択でフォルダを選択することができない。そのため複数ファイルを選択する場合ツリー構造の情報は除外される。そもそもひとつの Key-Value には同じ階層のファイルしか含めることができない。階層が違うファイルは別のプロパティとして設定する必要がある。 特にツリー構造の情報が必要ない場合はファイルだけを、ツリー構造の情報が必要な場合は別途その情報を設定して送信する。

f:id:yotiky:20210227012709p:plain

typeTextFile が選べる。File は1つでも複数でも選択できるようになっている。 File の Key-Value は複数定義すると1つの配列にまとめられて送信されるため、階層が違うファイルを選択する場合はそれぞれの階層に応じた Key-Value を増やせば一括で送信できる。

f:id:yotiky:20210227012748p:plain

サーバーサイドの実行結果。

[2021-02-26T17:45:20.643Z] C# HTTP trigger function processed a request.
[2021-02-26T17:45:20.676Z] PostmanRuntime/7.26.5
[2021-02-26T17:45:20.676Z] glanpalace1.jpg
[2021-02-26T17:45:20.677Z] aurora2.jpg
[2021-02-26T17:45:20.677Z] icelandlake2540.jpg
[2021-02-26T17:45:20.678Z] basedir(1): wallpapers
[2021-02-26T17:45:20.679Z] filePaths(3): glanpalace1.jpg,photo1/aurora2.jpg,photo1/icelandlake2540.jpg
[2021-02-26T17:45:20.689Z] Executed 'Upload' (Succeeded, Id=5172d86a-1074-40bb-a3d7-f4c793d35157, Duration=265ms)

f:id:yotiky:20210227024533p:plain

Azure Functions で Swagger UI

NSwag は NSwag.SwaggerGeneration.AzureFunctionsV2 が更新されておらず、V3ではエラーが出て動かなかったため、Swashbuckle を使用する。

目次

検証環境

  • Azure Functions v3
  • AzureExtensions.Swashbuckle v3.2.2

古いライブラリに注意

  • AzureFunctions.Extensions.Swashbuckle は更新されておらずエラーが出て動かない

github.com

実装

NuGet でライブラリをインストールする。

Statup クラスを追加する。

using AzureFunctions.Extensions.Swashbuckle;
...

[assembly: FunctionsStartup(typeof(Startup))]

namespace FunctionApp1
{
    public class Startup : FunctionsStartup
    {
        public override void Configure(IFunctionsHostBuilder builder)
        {
            builder.AddSwashBuckle(Assembly.GetExecutingAssembly());
        }
    }
}

Open API と Swagger UI の Functions を追加する。

public static class SwaggerFunctions
{
    [SwaggerIgnore]
    [FunctionName("Swagger")]
    public static Task<HttpResponseMessage> Run(
        [HttpTrigger(AuthorizationLevel.Function, "get", Route = "Swagger/json")] HttpRequestMessage req,
        [SwashBuckleClient] ISwashBuckleClient swashBuckleClient)
    {
        return Task.FromResult(swashBuckleClient.CreateSwaggerDocumentResponse(req));
    }

    [SwaggerIgnore]
    [FunctionName("SwaggerUi")]
    public static Task<HttpResponseMessage> Run2(
        [HttpTrigger(AuthorizationLevel.Function, "get", Route = "Swagger/ui")] HttpRequestMessage req,
        [SwashBuckleClient] ISwashBuckleClient swashBuckleClient)
    {
        return Task.FromResult(swashBuckleClient.CreateSwaggerUIResponse(req, "swagger/json"));
    }
}

対象の Functions に属性をつける。

public class Function1
{
    [QueryStringParameter("name", "User Name", DataType = typeof(string), Required = false)]
    [ProducesResponseType(typeof(string), (int)HttpStatusCode.OK)]
    [ProducesResponseType((int)HttpStatusCode.InternalServerError, Type = typeof(Error))]
    [FunctionName("Function1")]
    public async Task<IActionResult> Run(
        [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req,
        ILogger log)
    {
        log.LogInformation("C# HTTP trigger function processed a request.");

        string name = req.Query["name"];

        string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
        dynamic data = JsonConvert.DeserializeObject(requestBody);
        name = name ?? data?.name;

        string responseMessage = string.IsNullOrEmpty(name)
            ? "This HTTP triggered function executed successfully. Pass a name in the query string or in the request body for a personalized response."
            : $"Hello, {name}. This HTTP triggered function executed successfully.";

        return new OkObjectResult(responseMessage);
    }
}
public class Error
{
    public string Title { get; set; }

    public string Description { get; set; }
}

http://localhost:7071/api/Swagger/ui で Swagger UI にアクセス。

f:id:yotiky:20210224092449p:plain

参考

ASP.NET Core の場合に参考になりそうな記事。

Azure API Management

API Management (APIM) は、既存のバックエンドのサービスに対して一貫性のある最新の API ゲートウェイを迅速に作成する手段です。

API Management が組織にもたらす利点は、外部のパートナーや社内の開発者に API を公開することによって、社内に眠っているデータやサービスの可能性を発掘できることです。 どの企業も、その業務をデジタル プラットフォームで拡大し、新しい販路と顧客を開拓すると共に、既存の顧客との絆を深めようと模索しています。 API Management は、開発者の取り組み、ビジネス インサイト、分析、セキュリティ、保護を通じて API プログラムの価値を高め、企業にコア コンピテンシーをもたらします。 Azure API Management を任意のバックエンドで実行し、それに基づいて本格的な API プログラムを起動できます。

Azure Functions で Azure Cosmos DB (Table) を操作する

目次

検証環境

  • Azure Functions v3
  • Microsoft.Azure.Cosmos.Table v1.0.8

古いライブラリに注意

  • WindowsAzure.Storage は非推奨
  • Microsoft.Azure.CosmosDB.Table はまもなく非推奨

実装

NuGet でライブラリをインストールする。

using Microsoft.Azure.Cosmos.Table;

データ定義は、TableEntity を継承したクラスを作成する。 取得時にパラメータなしのコンストラクタが必要になる。

public class CustomerEntity : TableEntity
{
    public CustomerEntity() { }
    public CustomerEntity(string lastName, string firstName)
    {
        PartitionKey = lastName;
        RowKey = firstName;
    }

    public string Email { get; set; }
    public string PhoneNumber { get; set; }
}

CloudTableインスタンスを取得する。

var connectionString = "ConnectionString";
var tableName = "TableA";

var storageAccount = CloudStorageAccount.Parse(connectionString);
var tableClient = storageAccount.CreateCloudTableClient();
var cloudTalbe = tableClient.GetTableReference(tableName);

テーブル作成

// なければ作成する
await table.CreateIfNotExistsAsync(IndexingMode.Consistent, throughput: 400, null, CancellationToken.None);

プロビジョニングされたスループットの場合、throughput を指定せずに作成すると 800 で作成されてしまうので注意。最低コストの2倍発生する。 IndexingModedefaultTimeToLive はこの指定で手動で作成した時のデフォルト値と同じになる。

テーブルに設定されたスループットは、ポータル上の下図から確認できる。 f:id:yotiky:20210227183357p:plain

挿入

1件挿入する場合。

var entity = new CustomerEntity("Tanaka", "Taro")
{ 
    Email = "aaa", 
    PhoneNumber = "001"
};

await cloudTalbe.ExecuteAsync(TableOperation.InsertOrReplace(entity));

まとめて挿入する場合。

var entities = new[]
{
    new CustomerEntity("Tanaka", "Taro"){ Email = "aaa", PhoneNumber = "001"},
    new CustomerEntity("Tanaka", "Jiro"){ Email = "bbb", PhoneNumber = "002"},
};

var operation = new TableBatchOperation();
foreach (var entity in entities)
{
    operation.Add(TableOperation.InsertOrReplace(entity));
}

await cloudTalbe.ExecuteBatchAsync(operation);

取得

PartitionKeyとRowKeyを指定して1件取得する場合。

var entity = new CustomerEntity("Tanaka", "Taro")
{ 
    Email = "aaa", 
    PhoneNumber = "001"
};

var tableResult = await cloudTalbe.ExecuteAsync(TableOperation.Retrieve<CustomerEntity>(entity.PartitionKey, entity.RowKey));
var result = tableResult.Result as CustomerEntity;

クエリを使う場合。

var entity = new CustomerEntity("Tanaka", "Taro")
{ 
    Email = "aaa", 
    PhoneNumber = "001"
};


var query = new TableQuery<CustomerEntity>();
query.FilterString = TableQuery.CombineFilters(
    TableQuery.GenerateFilterCondition(nameof(CustomerEntity.PartitionKey), QueryComparisons.Equal, entity.PartitionKey),
    TableOperators.And,
    TableQuery.GenerateFilterCondition(nameof(CustomerEntity.RowKey), QueryComparisons.Equal, entity.RowKey));

var tableResult = await cloudTalbe.ExecuteQuerySegmentedAsync(query, null);
var results = tableResult.Results;

ソートしたい場合。

var where = TableQuery.GenerateFilterCondition(nameof(CustomerEntity.PartitionKey), QueryComparisons.Equal, entity.PartitionKey);

var query = new TableQuery<CustomerEntity>()
    .Where(where)
    .Order(nameof(CustomerEntity.Timestamp));

var tableResult = await cloudTalbe.ExecuteQuerySegmentedAsync(query, null);

cloudTalbe.CreateQuery<CustomerEntity>() を使おうとすると NotSupportedException が発生するので注意*1

削除

取得時の Entity が書き換わってないかチェックして削除する場合。

await cloudTalbe.ExecuteAsync(TableOperation.Delete(entity));

気にせず削除する場合は ETag にワイルドカードを使う。

var entity = new CustomerEntity("Tanaka", "Taro");
entity.ETag = "*";
await cloudTalbe.ExecuteAsync(TableOperation.Delete(entity));

参考

関連記事

Azure Functions で Azure Queue Storage を操作する

目次

検証環境

  • Azure Functions v3
  • Azure.Storage.Queues v12.6.0

実装

NuGet でライブラリをインストールする。

using Azure.Storage.Queues;
using Azure.Storage.Queues.Models;

キューの作成

var connectionString = "ConnectionString";
var queueName = "QueueName”;

var queueClient = new QueueClient(connectionString, queueName);

await queueClient.CreateAsync();

メッセージの追加

await queueClient.SendMessageAsync("First");
await queueClient.SendMessageAsync("Second");

// 最近はBinaryも追加できるらしい
var binary = new BinaryData(new byte[0]);
await queueClient.SendMessageAsync(binary);

メッセージの表示

Azure.Response でラップされてるので Value プロパティを参照する。

var message = await queueClient.PeekMessageAsync();
log.LogInformation(message.Value.MessageId + ":" + message.Value.MessageText);

var messages = await queueClient.PeekMessagesAsync(maxMessages:5);
foreach (var msg in messages.Value)
{
    log.LogInformation(msg.MessageId + ":" + msg.MessageText);
}

メッセージの更新

キューは積み直しになる。 ReceiveMessageAsync ではなく SendMessageAsync の戻り値を使って更新しても同じ。

var receipt = await queueClient.ReceiveMessageAsync();
await queueClient.UpdateMessageAsync(receipt.Value.MessageId, receipt.Value.PopReceipt, "Updated");

メッセージの受信

受信するとキューからは削除される。 Azure.Response でラップされてるので Value プロパティを参照する。

var message = await queueClient.ReceiveMessageAsync();
log.LogInformation(message.Value.MessageId + ":" + message.Value.MessageText);

複数件受信する場合。

var messages = await queueClient.ReceiveMessagesAsync(maxMessages: 5);
foreach (var msg in messages.Value)
{
    log.LogInformation(msg.MessageId + ":" + msg.MessageText);
}

メッセージの削除

ReceiveMessageAsync で消えてしまうので、PeekedMessagePopReceipt が取れないので使いみちあるのか謎。 SendMessageAsync 時に取り消すくらいには使えるかもしれない。

var message = await queueClient.ReceiveMessageAsync();
await queueClient.DeleteMessageAsync(message.Value.MessageId, message.Value.PopReceipt);

キューの削除

// キューが存在しない場合は例外発生
await queueClient.DeleteAsync();

// キューが存在しない場合に例外が出ない
await queueClient.DeleteIfExistsAsync();

その他

メッセージのエンコード

Azure Functions のキュートリガーが文字列を受け取る場合、Base64エンコードされた値が想定されている。エンコードされていない値だと例外が発生する。 そのためキューに追加する時に、Base64エンコードした文字列を設定する必要がある。

SDK を使って文字列を追加する場合、SDK の v12 と v11 以前では動作が異なるので注意が必要。 v11 以前では自動的にエンコードされていたが、v12 ではエンコードされなくなったため明示的にエンコードする。

関連記事