noxi雑記

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

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();
}

WijmoのSCSSを使用する

Wijmo のビルドインなカスタムテーマに Material テーマがあります。カスタムテーマは 2019v3 までは css ファイルまたは LESS で提供されていましたが、 2020v1 にて SCSS に切り替わりました。私は LESS を SCSS に手動翻訳して Angular Material と組み合わせて使用しているのですがこれを SCSS ベースのものに差し替えるべく、試しに導入します。

続きを読む

.NET CoreのSingle Fileを試す

.NET Core 3.0 で標準機能として実装された単一ファイル実行モード。リリースからはや9ヶ月、既に .NET 5 の足音が聞こえ始めており今更感が拭えませんが、単一ファイルにするとどうなるのか、どう実行されるのか、色々試してみたメモです。

docs.microsoft.com

続きを読む