noxi雑記

.NET、Angularまわりの小ネタブログ

.NET で MSAL トークンを X.509 証明書で取ってみる

この記事は .NET Core 3.1 のデーモンコンソールアプリで MSAL トークンをクライアント ID と X.509 証明書で取得してみた時の作業メモです。
やったこと:

  1. Azure Key Vault で証明書作成
  2. Azure AD アプリ作成
  3. .NET からトークン取得
続きを読む

Xamarin iOSアプリをAzure PipelinesでビルドしてAppCenterに送信する(配布編)

はじめに

Azure Pipelines 使っていますか? 昨今は GitHub Actions に後塵を拝している感が否めませんが、マルチステージを利用した承認機能など、実は結構便利に利用できます。今回は Xamarin.Forms で実装した iOS アプリをビルドするための Azure Pipelines を組んでみます。またビルド結果を AppCenter へ送信し、テスト配布も行います。

全体を一本の記事にするととても長くなってしまうためビルドとテスト配布の2つに分割しました。本記事は後編の配布編です。

ノンビリと書いていたら GitHub Actions にも承認処理が来てしまったような気がしますが、気にしたら負けです。

環境

今回試したものはこちらに置いてあります。

github.com

ビルドYAML全体

前編の記事でビルドした Xamarin iOS アプリを AppCenter へ送信する Pipeline YAML は全体でこのようになります。実際にはビルドと配布のマルチステージ構成となっていますので、 GitHub 上の YAML をご参照下さい。

variables:
  AppCenterAppSlug: 'AppCenterの識別名 "チーム名/アプリ名" '

pool:
  vmImage: 'ubuntu-latest' # 何でも良い

steps:

  # Pipeline Artifacts をダウンロード
  - download: current

  # AppCenter へビルドしたアプリを配信
  - task: AppCenterDistribute@3
    displayName: 'Distribute to AppCenter'
    inputs:
      serverEndpoint: 'AppCenterConnection'
      appSlug: '$(AppCenterAppSlug)'
      appFile: '$(Pipeline.Workspace)/drop/SampleApp.iOS.ipa'
      releaseNotesOption: 'input'
      releaseNotesInput: '$(Build.SourceVersionMessage)'
      destinationType: 'groups'
      isSilent: false

Azure Pipeline Tasks

download

downloadDownload Pipeline Artifact タスクのショートカットです。ドキュメントに記載の通りこのショートカットを使用すると $(Pipeline.Workspace)/ に Pipeline Artifacts がダウンロードされます。デプロイ系のジョブであれば自動で Pipeline Artifacts を持ってきてくれる気がしますが普通のジョブでは手動の点、注意が必要です。

docs.microsoft.com

AppCenterDistribute@3

ビルドで生成された *.ipa ファイルを AppCenter で配布するために AppCenter へと送信するタスクです。このタスクを使用するには AppCenter から取得したトークンを元に ServiceConnection を作成することが必須となります。また AppCenter のアプリ登録を {userName}/{appName} の形式で appSlug に設定します。先行タスクで Pipeline Artifacts から ipa ファイルをダウンロードしているので、 appFile にはダウンロードしたファイルのパスを指定します。
そしてちょっと面倒臭いことに AppCenter に登録するリリースノート情報が必須です。リリースノートの指定は releaseNotesOptionreleaseNotesInput または releaseNotesFile を使用します。サンプルではビルド対象のコミットのコミットメッセージが格納されている Azure Pipelines の Build.SourceVersionMessage 環境変数を固定で指定しています。

docs.microsoft.com

task: AppCenterDistribute@3
inputs:
  serverEndpoint: 'AppCenterConnection'
  appSlug: '$(AppCenterAppSlug)'
  appFile: '$(Pipeline.Workspace)/drop/SampleApp.iOS.ipa'
  releaseNotesOption: 'input'
  releaseNotesInput: '$(Build.SourceVersionMessage)'
  destinationType: 'groups'
  isSilent: false

AppCenter ServiceConnection

続いて AppCenter へ送信するために必要な ServiceConnection の登録方法です。まずはプロジェクトの設定画面から ServiceConnection を選択して新規作成を行います。種別は Visual Studio App Center を選択します。選択した後の設定値は表を参考にして下さい。

設定名 設定値
Server URL (元の値から変更しない)
API Token AppCenter の設定画面から FullAccess スコープで出力したトークンキー
Service connection name AppCenterDistribute タスクの serverEndpoint に指定する名前
Description お好みで

Xamarin iOSアプリをAzure PipelinesでビルドしてAppCenterに送信する(ビルド編)

Azure Pipelines 使っていますか? 昨今は GitHub Actions に後塵を拝している感が否めませんが、マルチステージを利用した承認機能など、実は結構便利に利用できます。今回は Xamarin.Forms で実装した iOS アプリをビルドするための Azure Pipelines を組んでみます。またビルド結果を AppCenter へ送信し、テスト配布も行います。

全体を一本の記事にするととても長くなってしまうためビルドとテスト配布の2つに分割しました。本記事は前編のビルド編です。

続きを読む

ApplicationInsightsの「エンドツーエンドトランザクションの詳細画面」を表示する

ApplicationInsights で記録したログが時系列に表示される「エンドツーエンドトランザクションの詳細画面」を検索画面を経ずに URL 生成して直接表示してみようという試みのメモです。エンドツーエンドトランザクションの詳細画面はコレ。

f:id:noxi515:20200922042325p:plain
エンドツーエンドトランザクションの詳細画面

この画面の URL はパスにエスケープされた JSON を2つ含んでおり、見やすくエスケープとフォーマットをしてみます。

https://portal.azure.com/#blade/AppInsightsExtension/DetailsV2Blade/DataModel/
{
  "eventId": "00000000-0000-0000-0000-000000000000",  // 対象レコードの itemId
  "timestamp": "2020-09-21T10:23:57.159Z",            // 対象レコードの timestamp
  "cacheId": "00000000-0000-0000-0000-000000000000",  // 不明
  "eventTable": "exceptions",                         // 対象レコードの itemType
  "timeContext": {                                    // 日時検索範囲
    "durationMs": 86400000,                           // 検索期間ミリ秒
    "endTime": "2020-09-21T19:21:00.000Z",            // 検索終了日時
    "createdTime": "2020-09-21T19:20:14.460Z",        // 検索条件が作成された日時?
    "isInitialTime": false,                           // 不明
    "grain": 1,                                       // 不明
    "useDashboardTimeRange": false                    // 不明
  }
}
/ComponentId/
{
  "Name": "ApplicationInsightsの名前",
  "SubscriptionId": "サブスクリプションID",
  "ResourceGroup": "リソースグループの名前"
}

この中から実際に画面を叩いてみて必要だったパラメーターは次の6つでした。

  • eventId
  • timeContext / durationMs
  • timeContext / endTime
  • Name
  • SubscriptionId
  • ResourceGroup

詳細画面を直接開くURLは例えばこのように生成できます。

// JSの場合

const aiName = 'ApplicationInsights Resource Name';
const subscriptionId = '00000000-0000-0000-0000-000000000000';
const resourceGroupName = 'ResourceGroup Name';
const eventId = '00000000-0000-0000-0000-000000000000'; // 対象レコードID
const timestamp = new Date('2020-09-22T04:43:00+09:00'); // 対象レコード日時
const durationMs = 24 * 60 * 60 * 1000; // 1日 = 86400000
const endTime = new Date(timestamp.getTime() + durationMs / 2); // 表示終了日時 = 対象レコードから12時間後

const dataModel = JSON.stringify({ eventId, timeContext: { durationMs, endTime } });
const componentId = JSON.stringify({ Name: aiName, SubscriptionId: subscriptionId, ResourceGroup: resourceGroupId });

const url = 'https://portal.azure.com/#blade/AppInsightsExtension/DetailsV2Blade/DataModel/'
  + encodeURIComponent(dataModel)
  + '/ComponentId/'
  + encodeURIComponent(componentId);

System.Text.Jsonのコンバーターメモ

データを POST するときは文字列、 GET するときはオブジェクトという C# 泣かせの構造な API に出会ってしまったため、 System.Text.Json の Converter を作った時のメモです。

公式のドキュメントはこちら。

docs.microsoft.com


やりたいこと

冒頭に記述したように、今回の対象は POST と GET で異なる型の C# 泣かせな JSON です。
実際の JSON はこのようになっています。

// POST
{
  "key": "abcde"
}

// GET
{
  "key": {
    "link": "https://xxxxxx/abcde",
    "value": "abcde
  }
}

これを Converter を駆使してイイカンジに POCO にマッピングします。

// 対象の POCO
public class LinkObject
{
    public string? Link { get; set; }
    public string? Value { get; set; }
}

Converter を実装する

冒頭にリンクを張ったドキュメントによると System.Text.Json の Converter 仕様は色々あるようです。今回は複雑なものでは無いためシンプルに JsonConverter<T> を実装します。

Converter のシリアライズ実装

まずは POCO から JSONシリアライズする処理を実装します。実装方法はシンプルで、 Utf8JsonWriter を使って JSON の構造を記述していきます。文字列を1つ書き込むだけなので難しいことはせず、 null or not で処理がちょっと違うだけです。

public override void Write(Utf8JsonWriter writer, LinkObject value, JsonSerializerOptions options)
{
    if (value.Value == null)
    {
        writer.WriteNullValue();
        return;
    }

    writer.WriteStringValue(value.Value);
}

ちょっと分からなかったのは null の除外方法です。 JsonSerializerOptionsIgnoreNull を指定すると null を JSON に出力させない設定が可能ですが、 Converter に入ってきた時点でプロパティのキーは書き込まれてしまっているため、 オブジェクトは null ではないが状態としては null の場合は null を書き込まざるをえませんでした。

Converter のデシリアライズ実装

続いて JSON から POCO へのデシリアライズ処理を実装します。 Utf8JsonReader はいわゆる? PullParser 形式で、 JSON の構成要素を一つ一つ順番に読んでいきます。その時点でのトークンを見ながらオブジェクトにマッピングしていきます。

public override LinkObject Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
    // objectスコープに入っていることをチェック
    if (reader.TokenType != JsonTokenType.StartObject)
    {
        throw new JsonException("Invalid TokenType");
    }

    // Readを繰り返してobjectスコープの終わりまで読む
    var obj = new LinkObject();
    while (reader.Read())
    {
        if (reader.TokenType == JsonTokenType.EndObject)
        {
            return obj;
        }

        if (reader.TokenType != JsonTokenType.PropertyName)
        {
            continue;
        }

        var propName = reader.GetString().ToLower();

        if (!reader.Read())
        {
            throw new JsonException();
        }

        var value = reader.TokenType switch
        {
            JsonTokenType.Null => null,
            JsonTokenType.String => reader.GetString(),
            _ => throw new JsonException()
        };

        switch (propName)
        {
            case "value":
                obj.Value = value;
                break;

            case "link":
                obj.Link = value;
                break;

            default:
                throw new JsonException("Unknown property name");
        }
    }

    throw new JsonException();
}