Quantcast
Channel: かずきのBlog@hatena
Viewing all 1388 articles
Browse latest View live

Uno Platform から JavaScript のライブラリを使いたい

$
0
0

日本語情報だとひらりんさんのブログにまとまってます!

himanago.hatenablog.com

私がはまったのは、iziToast というライブラリでトースト出そうとして

function showToast(text) {
    iziToast.show({ title: 'Info', message: text });
}

という関数を JS に作って C# 側から以下のように呼んでも iziToast が undefined になっていてエラーになる感じです。iziToast.js と css は WasmScripts と WasmCSS フォルダーにおいて埋め込みリソースに設定していてもダメでした。

WebAssemblyRuntime.InvokeJS($"showToast('{text}');");

解決策

iziToast.js をステップ実行していると最初の iziToast の宣言をする処理が以下のようになっていたのですが

f:id:okazuki:20200114005804p:plain

最初の if 文に入っていて define 関数で定義されるようなルートを通ってました。require.js が Uno Platform の WASM の実行時には読み込まれているようで(実際に Chrome の開発者モードで見てみるとそうなってました)特に名前指定なしで define したときはファイル名で require しないといけないみたいなんですね。

qiita.com

なので、JavaScript の showToast を以下のようにして require するようにしたら無事 iziToast を Uno Platform から使えました。

// こんな感じかfunction showToast(text) {
    require(['iziToast'], function (iziToast) {
        iziToast.show({ title: 'Info', message: text });
    });
}// こっちでも OKconst iziToast = require('iziToast');
function showToast(text) {
    iziToast.show({ title: 'Info', message: text });
}

ちゃんとトースト出てますね。

f:id:okazuki:20200114010035p:plain

iziToast のサイト

https://izitoast.marcelodolza.com/


ReactiveProperty v6.2.0 をリリースしました

$
0
0

ReactiveProperty の DataAnnotations によるバリデーションのエラーメッセージの改善をしました。

今まで

以下のような定義のプロパティで

[Required(ErrorMessage = "{0} は必須入力項目です。")]
public ReactiveProperty<string> Name { get; }

というようなアノテーションをつけたときのエラーメッセージは「Value は必須入力項目です」でした。

今回の対応

「Name は必須入力項目です」になります。

さらに

[Required(ErrorMessage = "{0} は必須入力項目です。")]
[Display(Name = "名前")]
public ReactiveProperty<string> Name { get; }

にすると「名前 は必須入力項目です。」になります。 その他にもリソースにも対応しているので、エラーメッセージの国際化対応がやりやすくなっています。

各種リンク

NuGet パッケージはこちら

www.nuget.org

リポジトリーはこちら。何かあったら Issue 立ててください。日本語でも英語でも OK です。

github.com

空の IObservable からの ReactiveProperty と ReactiveCommand の生成

$
0
0

先日、こんな質問を頂きました。

とりあえず値が来てないシーケンスで作ってそうに見えたので回答したのですが、確かに慣れてないと理解に時間がかかるなぁと思ったので、ついでにブログネタにしてしまおうという魂胆です。

単純化してみよう

ということで、ReactiveProperty や ReactiveCommand は IObservable<T>から ToReactiveProperty や ToReactiveCommand メソッドを使って生成できます。 例えば…

using Reactive.Bindings;
using System;
using System.Reactive.Linq;

namespace ConsoleApp9
{
    class Program
    {
        staticvoid Main(string[] args)
        {
            // Subscribe すると false を返す IObservalbe<bool>
            var seq = Observable.Return(false);

            // ReactiveProperty と ReactiveCommand を生成
            var rp = seq.ToReactiveProperty();
            var rc = seq.ToReactiveCommand();

            Console.WriteLine(rp.Value); // False
            Console.WriteLine(rc.CanExecute()); // False
        }
    }
}

こんな風に false を返すシーケンスから作ると、どちらも false になります。

では、空のシーケンスで試してみましょう。以下のような感じになります。

using Reactive.Bindings;
using System;
using System.Reactive.Linq;

namespace ConsoleApp9
{
    class Program
    {
        staticvoid Main(string[] args)
        {
            // Subscribe しても何も返さないシーケンス
            var seq = Observable.Never<bool>();

            // ReactiveProperty と ReactiveCommand を生成
            var rp = seq.ToReactiveProperty();
            var rc = seq.ToReactiveCommand();

            Console.WriteLine(rp.Value); // False
            Console.WriteLine(rc.CanExecute()); // True
        }
    }
}

ReactiveProperty のケース

ReactiveProperty はシーケンスから値が発行されていない状態でも、何らかの値は Value プロパティに保持していないといけません。値を返さずに例外を投げる(例えば独自例外で、ReactiveProperty に値がないよ!っていうのを表すものを作って、それをスローするとか)というのも考えられますが、ReactiveProperty の使われ方的に Value にアクセスしたら例外が出るケースがあるというのは使いにくさの割にメリットがないです。

ということで、ReactiveProperty は初期値を持ってます。初期値なんて設定した覚えないんだけど??となるかもしれませんが、ToReactiveProperty 拡張メソッドの定義を見てみると第二引数にあります。

ReactiveProperty/ReactiveProperty.cs at e3640f66e0e04b43b77dc44029fc416de8c060f3 · runceel/ReactiveProperty · GitHub

/// <summary>/// <para>Convert to two-way bindable IObservable&lt;T&gt;</para>/// <para>PropertyChanged raise on ReactivePropertyScheduler</para>/// </summary>publicstatic ReactiveProperty<T> ToReactiveProperty<T>(this IObservable<T> source,
            T initialValue = default(T),
            ReactivePropertyMode mode = ReactivePropertyMode.DistinctUntilChanged | ReactivePropertyMode.RaiseLatestValueOnSubscribe, IEqualityComparer<T> equalityComparer = null) =>
            new ReactiveProperty<T>(source, initialValue, mode, equalityComparer);

T initialValue = default(T)の部分です。この initialValue が ReactiveProperty のコンストラクターに渡されて、最終的に何も値が来てないときの初期値として設定されます。

そのため、空のシーケンスに対して ToReactiveProperty をすると bool 型の場合は default(bool) の結果である false が初期値として設定されます。結果として Console.WriteLine(rp.Value)は False と表示されます。

ReactiveCommand のケース

ReactiveCommand は IObservable<bool>の結果を CanExecute として返します。ReactiveCommand は、空のシーケンスから作られたときの初期値として default(bool) ではなく true を使っています。 これは ReactiveCommand が使われるケースで、普通に new ReactiveCommand()したときに実行できないコマンドよりも実行可能なコマンドが求められるケースのほうが圧倒的に多いという理由で、CanExecute の戻り値は初期値が true になっています。

ToReactiveCommand の定義を見てみても明示的に initialValue は true になっています。

ReactiveProperty/ReactiveCommand.cs at e3640f66e0e04b43b77dc44029fc416de8c060f3 · runceel/ReactiveProperty · GitHub

/// <summary>/// CanExecuteChanged is called from canExecute sequence on UIDispatcherScheduler./// </summary>publicstatic ReactiveCommand ToReactiveCommand(this IObservable<bool> canExecuteSource, bool initialValue = true) =>
            new ReactiveCommand(canExecuteSource, initialValue);

そのため、空のシーケンスに対して ToReactiveCommand を呼び出すと、実行可能なコマンド(CanExecute の結果が true) が出来上がります。

今回のケースに当てはめてみよう

再現コードを GitHub に上げてもらうようにお願いしたら上げてもらえました!

GitHub - AdonisLeavis/WpfApp1: ReactiveCommandの動きを確認するプロジェクト

該当部分は ViewModels フォルダーの中の MainWindowViewModel クラスになります。

<feff>using Reactive.Bindings;
using System.Reactive.Linq;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Reactive.Bindings.Extensions;

namespace WpfApp1.ViewModels
{
    publicclass MainWindowViewModel : Bases.ViewModelBase
    {
        public ReactiveProperty<CellViewModel> CurrentCell { get; set; } = new ReactiveProperty<CellViewModel>();

        public ReactiveCommand SetCurrentCellCommand { get; set; } = new ReactiveCommand();
        public ReactiveCommand ClearCurrentCellCommand { get; set; } = new ReactiveCommand();
        public ReactiveCommand TestCommand { get; set; }

        public MainWindowViewModel()
        {
            SetCurrentCellCommand.Subscribe(_ =>
            {
                var flag = false;
                if(CurrentCell.Value != null)
                {
                    flag = !CurrentCell.Value.IsFirstFlag;
                }
                var curCell = new CellViewModel() { IsFirstFlag = flag };
                CurrentCell.Value = curCell;
            });

            ClearCurrentCellCommand.Subscribe(_ => CurrentCell.Value = null);

            var testProp = new[]
            {
                CurrentCell.Select(a => a!=null),
                CurrentCell.Where(a => a != null).Select(a => a.IsFirstFlag),
                // CurrentCell.Select(a => a != null && a.IsFirstFlag), ★ こっちだと正しく動作する
            }.CombineLatestValuesAreAllTrue().ToReactiveProperty();
            testProp.Subscribe(a => System.Console.WriteLine($"来たよ{a}"));

            TestCommand = new[]
            {
                CurrentCell.Select(a => a!=null),
                CurrentCell.Where(a => a != null).Select(a => a.IsFirstFlag),
                // CurrentCell.Select(a => a != null && a.IsFirstFlag), ★ こっちだと正しく動作する
            }.CombineLatestValuesAreAllTrue().ToReactiveCommand();
            TestCommand.Subscribe(_ => System.Console.WriteLine(CurrentCell.Value.IsFirstFlag));
        }
        
    }
}

TestCommand が今回の質問の現象が起きているクラスになります。CombineLatestValuesAreAllTrue メソッドを呼び出した結果に対して ToReactiveCommand をしてます。 CombineLatestValuesAreAllTrue 拡張メソッドは、CombineLatest で全てが true かどうかをチェックしているだけのメソッドです。

以下のように定義されています。

/// <summary>/// Lastest values of each sequence are all true./// </summary>publicstatic IObservable<bool> CombineLatestValuesAreAllTrue(
            this IEnumerable<IObservable<bool>> sources) =>
            sources.CombineLatest(xs => xs.All(x => x));

もとになっている CombineLatest メソッドとはどんなメソッドかというと、sourcesの全ての IObservable<T>の最後の値に対して、処理をした結果を後続に流す IObservable<T>を返すメソッドになります。 試しに以下のようなコードを書いてみます。

// Subscrie すると true を返すシーケンス
var source1 = Observable.Return(true);
var source2 = Observable.Return(true);

// CombineLatest で and をとった結果を返すシーケンスにする
var result = new[] { source1, source2 }.CombineLatest(xs => xs[0] & xs[1]);
// 表示
result.Subscribe(x => Console.WriteLine(x)); // True

CombineLatest で and をとってます。今回は True を返すシーケンスを 2 つ合成しているので True & Trueをして True になりますね。どちらかでも Observable.Return(false)にすると false になります。

時間の経過とともに値を発行していくと…?

固定の値を返す IObservable<T>の時の動作はわかりました。次は時間の経過とともに値が発行されるもので試してみます。

// bool を発行する IObservable<bool> を 2 つ用意
var source1 = new Subject<bool>();
var source2 = new Subject<bool>();

// CombineLatest で and をとった結果を返すシーケンスにする
var result = new[] { source1, source2 }.CombineLatest(xs => xs[0] & xs[1]);
// 表示
result.Subscribe(x => Console.WriteLine($"Result = {x}"));

// 実際に値を発行していきます
Console.WriteLine("source1.OnNext(true)");
source1.OnNext(true);
Console.WriteLine("source2.OnNext(true)");
source2.OnNext(true);
Console.WriteLine("source1.OnNext(false)");
source1.OnNext(false);
Console.WriteLine("source1.OnNext(true)");
source1.OnNext(true);

実行するとどうなるでしょう?Subject 型は OnNext で値を発行しない限り値は発行されません。そのため Subscribe した時点では、まだ何も値が来てないので、Subscribe しただけでは Console.WriteLine は実行されません。コードの公判の OnNext をしているところで source1 で True、source2 で True を発行したタイミングで初めて CombineLatest の結果を Subscribe したところに処理が来ます。後は、source1 に対して false、true と値の発行をしてるので、そのたびに CombineLatest が実行されます。

つまり結果はこうなります。

source1.OnNext(true)
source2.OnNext(true)
Result = True
source1.OnNext(false)
Result = False
source1.OnNext(true)
Result = True

両方の値がそろって初めて CombineLatest の後ろに値が流れます。試しに source1 からだけ値を何回も発行するようにコードを書き換えて…

// bool を発行する IObservable<bool> を 2 つ用意
var source1 = new Subject<bool>();
var source2 = new Subject<bool>();

// CombineLatest で and をとった結果を返すシーケンスにする
var result = new[] { source1, source2 }.CombineLatest(xs => xs[0] & xs[1]);
// 表示
result.Subscribe(x => Console.WriteLine($"Result = {x}"));

// 実際に値を発行していきます
Console.WriteLine("source1.OnNext(true)");
source1.OnNext(true);
Console.WriteLine("source1.OnNext(false)");
source1.OnNext(false);
Console.WriteLine("source1.OnNext(true)");
source1.OnNext(true);
Console.WriteLine("source1.OnNext(false)");
source1.OnNext(false);
Console.WriteLine("source1.OnNext(true)");
source1.OnNext(true);
Console.WriteLine("source1.OnNext(false)");
source1.OnNext(false);

実行しても CombineLatest の結果の Subscribe の処理は実行されません。以下のような実行結果になります

source1.OnNext(true)
source1.OnNext(false)
source1.OnNext(true)
source1.OnNext(false)
source1.OnNext(true)
source1.OnNext(false)

これを踏まえてコードを見てみよう

TestCommand の生成は以下のように行われています。

            TestCommand = new[]
            {
                CurrentCell.Select(a => a!=null),
                CurrentCell.Where(a => a != null).Select(a => a.IsFirstFlag),
                // CurrentCell.Select(a => a != null && a.IsFirstFlag), ★ こっちだと正しく動作する
            }.CombineLatestValuesAreAllTrue().ToReactiveCommand();

CurrentCell は ReactiveProperty<CellViewModel>型になります。この CurrentCell プロパティは初期値が設定されていないのでデフォルトで null が入っています。 一つ目の CurrentCell.Select(a => a != null)は初期状態だと null が流れてくるので true が返されています。しかし、二つ目のほうは CurrentCell.Where(a => a != null).Select(a => a.IsFirstFlag)としてるため、初期値の null は Where 句で弾かれてしまい後ろの Select は実行されないので、初期状態では 何も値を発行しないシーケンスになります。

CombineLatest は、すべてのシーケンスの値が出そろわないと何もしないので、初期状態では空の IObservable<bool>になります。そのため TestCommand の CanExecute メソッドはデフォルト値の true を返します。そのため、初期状態では実行可能なコマンドになっています。

「★ こっちだと正しく動作する」のコメントに書かれている方は、CurrentCell の初期値の null がわたってきたときには false を返します。そのため初期状態では false を返すシーケンスに対して ToReactiveCommand をするので CanExecute メソッドの戻り値は false になります。そのため実行不可能なコマンドが出来上がります。

今回したかったことは?

具体的な仕様はわかりませんが、IsFirstFlag は false > true > false > true > false > ... のように変わっていく値のように見えます。SetCurrentCellCommand で、そのような制御が行われています。

            SetCurrentCellCommand.Subscribe(_ =>
            {
                var flag = false;
                if(CurrentCell.Value != null)
                {
                    flag = !CurrentCell.Value.IsFirstFlag;
                }
                var curCell = new CellViewModel() { IsFirstFlag = flag };
                CurrentCell.Value = curCell;
            });

このプログラムは、今回の問題調査用に最小限に絞られたプロジェクトなので、全体最適化という観点で見たら違う実装になるかもしれませんが、今回与えれらた情報だけで自分で作るなら MainWindowViewModel クラスは以下のようになるかな…と思いました。まぁ、基本的に★コメントで示された、こっちだと正しく動作するというのと同じです。C# の新しめの書きかた使った方がシンプルかな?って感じ。

using Reactive.Bindings;
using System.Reactive.Linq;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Reactive.Bindings.Extensions;
using Reactive.Bindings.Notifiers;
using System.Diagnostics;

namespace WpfApp1.ViewModels
{
    publicclass MainWindowViewModel : Bases.ViewModelBase
    {
        public ReadOnlyReactiveProperty<CellViewModel> CurrentCell { get; }

        public ReactiveCommand SetCurrentCellCommand { get; } = new ReactiveCommand();
        public ReactiveCommand ClearCurrentCellCommand { get; } = new ReactiveCommand();
        public ReactiveCommand TestCommand { get; }

        public MainWindowViewModel()
        {
            CurrentCell = Observable.Merge(
                SetCurrentCellCommand
                    .Select(_ => new CellViewModel { IsFirstFlag = (!CurrentCell.Value?.IsFirstFlag) ?? false }),
                ClearCurrentCellCommand
                    .Select(_ => default(CellViewModel))
            ).ToReadOnlyReactiveProperty();

            TestCommand = CurrentCell.Select(x => x?.IsFirstFlag ?? false)
                .ToReactiveCommand()
                .WithSubscribe(() => Debug.WriteLine(CurrentCell.Value.IsFirstFlag));
        }
    }
}

オリジナルリポジトリーを Fork して書き換えました。

GitHub - runceel/WpfApp1: ReactiveCommandの動きを確認するプロジェクト

コードスメル

ja.wikipedia.org

とりあえずせっかくソースコードもらったので、思ったことをつらつらと書いておきます。

  • 今回の IsFirstFlag は、名前からして最初の要素だということを表してるのかな?と思ったら違ったので名前は違う方がよさそう
  • もし、IsFirstFlag が裏の処理にも密接にかかわってくるなら IsFirstFlag の値の制御は、ViewModel よりは Model でやっといたほうがいいのでは…?と思った。もしくは、この画面だけの都合なら、CellViewModel のステートにせずに MainWindowViewModel のステートってだけでもいいのではと思った。
  • C# の新しい書きかたは使えるところではガンガン使おう。

まとめ

ということで、今回質問を受けて、確かにわかりにくいなぁと思ったのでまとめました。 それでは楽しいリアクティブプログラミングを!

Android で Azure Spatial Anchors + MRTK v2.2.0 を使うと DllNotFoundException が出る

$
0
0

adb.exe logcat -s Unity:*でログを見てると以下のようなエラーが出ます。

01-20 17:33:51.818 12729 12750 D Unity   : Unable to load library '/data/app/com.DefaultCompany.MRTKNuGetTest-yCg6ywpRyWRRM6lxBYRoYQ==/lib/arm/libazurespatialanchorsndk.so', native render plugin support disabled: java.lang.UnsatisfiedLinkError: dlopen failed: cannot locate symbol "ArImage_getFormat" referenced by "/data/app/com.DefaultCompany.MRTKNuGetTest-yCg6ywpRyWRRM6lxBYRoYQ==/lib/arm/libazurespatialanchorsndk.so"...
01-20 17:33:51.820 12729 12750 E Unity   : Unable to find azurespatialanchorsndk
01-20 17:33:51.835 12729 12750 I Unity   : System.DllNotFoundException: azurespatialanchorsndk
01-20 17:33:51.835 12729 12750 I Unity   :   at (wrapper managed-to-native) Microsoft.Azure.SpatialAnchors.NativeLibrary.ssc_cloud_spatial_anchor_create(intptr&)

各関係するパッケージのバージョンの不一致のせいでおきてます。 とりあえず ARCore XR Plugin の 2.1.2 を使ってたら置きました。解決方法は 2.2.0-preview のほうに上げることです。

動いてるバージョンの組み合わせ。

Window → Package Manager から入れたもの

  • ARFoundation 2.1.4
  • AR Subsystem preview.3 2.2.0
  • ARCore XR Plugin preview.2 - 2.2.0 (最初は 2.1.2 使っててダメだった)

NuGet から入れたもの

  • Microsoft.MixedReality.Toolkit.Foundation 2.2.0
  • Microsoft.MixedReality.Toolkit.Extensions 2.2.0

.unitypackage で入れたもの

まとめ

バージョン合わせ沼辛い。

az コマンドでアクセストークンをゲットする

$
0
0

流石のツイート。流石No1。

ということで、備忘録もかねて自分でもここで紹介されているコマンドを使って Graph API にアクセスするためのキーをとってみたいと思います。

> az account get-access-token --resource https://graph.microsoft.com
{
  "accessToken": "見ちゃダメ",
  "expiresOn": "2020-01-28 10:31:44.795950",
  "subscription": "見ちゃダメ",
  "tenant": "見ちゃダメ",
  "tokenType": "Bearer"
}

取れた!試しに MS Graph API 叩いてみましょう。

最近、簡単な API 叩くときには VS Code の REST Client 使ってるのですが手軽で便利です。

blog.okazuki.jp

以下のような感じで書くと…

GET https://graph.microsoft.com/v1.0/me
Authorization: Bearer ここにさっき手に入れた accessToken の値を入れる
Content-Type: application/json

こんな結果が返ってきました。ばっちり。

HTTP/1.1 200 OKCache-Control: privateContent-Type: application/json;odata.metadata=minimal;odata.streaming=true;IEEE754Compatible=false;charset=utf-8
Content-Encoding: gzipVary: Accept-Encodingrequest-id: xxxxxclient-request-id: xxxxxx-ms-ags-diagnostic: {"ServerInfo":{"DataCenter":"Japan East","Slice":"SliceC","Ring":"2","ScaleUnit":"002","RoleInstance":"AGSFE_IN_3"}}OData-Version: 4.0
Strict-Transport-Security: max-age=31536000
Date: Tue, 28 Jan 2020 00:37:15 GMTConnection: closeContent-Length: 429

{"@odata.context": "https://graph.microsoft.com/v1.0/$metadata#users/$entity",
  "businessPhones": ["秘密"
  ],
  "displayName": "Kazuki Ota",
  "givenName": "Kazuki",
  "jobTitle": "PFE",
  "mail": "秘密ではないけど、ここには書かない",
  "mobilePhone": "秘密",
  "officeLocation": "TOKYO-SHINAGAWA/24DFS",
  "preferredLanguage": null,
  "surname": "Ota",
  "userPrincipalName": "秘密ではないけど、ここには書かない",
  "id": "GUID が書いてあったよ"
}

いいね!ばっちり!

まとめ

以下のブログは今後もしっかりウォッチしておこうと思いました。

blog.azure.moe

【eLV】Cross-Platform 開発 シリーズで Uno Platform の登壇してきました

$
0
0

【eLV】Cross-Platform 開発 シリーズ で Uno Platform 触ってみたというタイトルで登壇してきました。

www.slideshare.net

話したのは10分くらいで15分以上デモをしていました。個人的に好きな UWP と同じ書き味で Android, iOS, Windows 10, ブラウザー(WebAssembly)や、最近は macOS まで!?単一ソースコードで作れるので非常に気に入ってます。

技術書典で本出します。ひらりんさんとちょまどさんと共著です!絶賛執筆中です!

himanago.hatenablog.com

React Native for Windows で metro のエラーが出る時の対応方法

$
0
0

こんなエラーが出てました。そして解決もしました。自分以外にこのエラーに会う人は稀だと思うけど自分用にメモ。

bundling failed: ReferenceError: SHA-1 for file C:\xxxxx\xxxx.js (C:\Users\xxxx\Documents\xxxx\xxxx.js) is not computed

f:id:okazuki:20200206184530p:plain

エラーメッセージの最初のファイル名と括弧の中のファイル名が違うのがポイント。

原因

私は、ドキュメントフォルダー以下にある Repos と Labs フォルダーをシンボリックリンクを張って C:\Repos と C:\Labs でアクセスできるようにしてます。理由は npm とか Xamarin とかでパスが長すぎてエラーになる問題への対応のためです。

blog.okazuki.jp

少し前にも、この運用のせいでエラーにあいました。 確か Blazor の WebAssembly のデバッグだったかな?今回も、シンボリックリンクと実フォルダーのパスをyarn startで起動したやつと、RN4W の UWP アプリが別々のほうを見に行ってるみたいで、エラーになっていたみたいです。

ちゃんと実フォルダーにプロジェクトを作るとうまくいきました。

シンボリックリンク辞めようかな…。

UWP で EntityFramework Core 3.1.x を使う方法

$
0
0

かな~~~~り昔にやったのと変わってたのでメモ。

NuGet パッケージ

最低限だけでよければこれだけ。

  • Microsoft.EntityFrameworkCore.Sqlite

下準備

適当に DbContext を継承したクラスを作ります。

using Microsoft.EntityFrameworkCore;
using System;
using System.IO;

namespace SQLiteApp
{
    publicclass MyContext : DbContext
    {
        public DbSet<Person> People { get; set; }

        protectedoverridevoid OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder.UseSqlite(
                "data source=sample.db"
            );
        }
    }

    publicclass Person
    {
        publicint Id { get; set; }
        publicstring Name { get; set; }
    }
}

そして、OnLaunched の先頭に以下のコードを追加します。

SQLitePCL.Batteries_V2.Init();
SQLitePCL.raw.sqlite3_win32_set_directory(/*data directory type*/1, Windows.Storage.ApplicationData.Current.LocalFolder.Path);
SQLitePCL.raw.sqlite3_win32_set_directory(/*temp directory type*/2, Windows.Storage.ApplicationData.Current.TemporaryFolder.Path);

using (var context = new MyContext())
{
    context.Database.EnsureCreated();
}

後は好きに使おう。マイグレーションまわりがどうなってるのかは未確認。


Bot Framework で全てのダイアログが終わったタイミングで処理がしたい

$
0
0

ダイアログの BeginDialogAsyncEndDialogAsyncをオーバーライドすると、ダイアログの最初と最後に処理をさしこめるっぽい。

でも、ダイアログの外側(Botクラス)からダイアログの状態を見て全てのダイアログが終わったタイミングで何かメッセージを送りたいといった場合は DialogState の DialogStack に現在のダイアログの状態が入ってるみたいなので、そこがからっぽだとダイアログの処理が綺麗に終わったということで、Bot クラスの OnMessageActivityAsync に以下のような処理を書くと Bot クラス側でダイアログがつつがなく全部終わった後に何かすることが出来そうです。

protectedoverride async Task OnMessageActivityAsync(ITurnContext<IMessageActivity> turnContext, CancellationToken cancellationToken)
{
    // Run the Dialog with the new message Activity.
    var accessor = ConversationState.CreateProperty<DialogState>(nameof(DialogState));
    await Dialog.RunAsync(turnContext, accessor, cancellationToken);
    var state = await accessor.GetAsync(turnContext);
    if (!state.DialogStack.Any())
    {
        // ダイアログが全部終わってるときの処理
    }
}

正攻法が他にあったら教えて。

Azure DevOps の Pipeline のハローワールドからビルド・単体テスト・デプロイ・スワップまで

$
0
0

Azure Pipeline についてちょっと見てみましょう。最近は、YAML で書けるので雰囲気としては GitHub Actions と同じ感じでいけます。というか同じチームで作ってるみたいなので、当然ですよね。

というわけで Pipeline を作っていきます。まずはハローワールドから。Azure Pipeline の画面を開くと最初は何もないのでパイプライン作らないか?って聞かれます。こんな感じで

f:id:okazuki:20200223180239p:plain

次にリポジトリーを選択するような画面になるのでお好みのリポジトリーを選択。このリポジトリーがデフォルトで clone されてきます。

そうすると、いきなりこんな感じの YAML が生成されます。

# Starter pipeline# Start with a minimal pipeline that you can customize to build and deploy your code.# Add steps that build, run tests, deploy, and more:# https://aka.ms/yamltrigger:- master

pool:vmImage:'ubuntu-latest'steps:- script: echo Hello, world!
  displayName:'Run a one-line script'- script: |
    echo Add other tasks to build, test, and deploy your project.
    echo See https://aka.ms/yaml
  displayName:'Run a multi-line script'

そのまま Save and run のボタンを押して実行してみます。しばらくすると動いて結果が表示されます。

f:id:okazuki:20200223180708p:plain

View raw log という部分を表示すると生のログが見れます。その中を確認するとパイプライン内で書いてある echo の出力がありますね。ばっちり。

f:id:okazuki:20200223180813p:plain

トリガー

ということで、このパイプラインですが何をきっかけにして動くの?というのがありますね。そこらへんを制御してるのが YAML の先頭にある trigger になります。

trigger:- master

これは、マスターブランチの変更をきっかけにパイプラインが動くというような定義です。他のブランチ名も配列に追加することで、複数のブランチをきっかけに動くパイプラインに出来ます。例えば以下のように YAML を書き換えると…

# Starter pipeline# Start with a minimal pipeline that you can customize to build and deploy your code.# Add steps that build, run tests, deploy, and more:# https://aka.ms/yamltrigger:- master
- release/*

pool:vmImage:'ubuntu-latest'steps:- script: echo Hello, world!
  displayName:'Run a one-line script'- script: |
    echo Add other tasks to build, test, and deploy your project.
    echo See https://aka.ms/yaml
  displayName:'Run a multi-line script'

release/v1.0という感じのブランチでも実行されます。例えば以下のような操作をすると…

> git checkout -b release/v1.0
> git commit --allow-empty -m "for v1.0"
> git push --set-upstream origin release/v1.0

Azure Pipeline の実行履歴に以下のように表示されます。

f:id:okazuki:20200223182257p:plain

ばっちりですね。ちなみに includeexcludeを使ってトリガーに含めるもの、除外するものを定義することが出来ます。ドキュメントの例そのままですが release/old/*は含めないケースは以下のようになります。

trigger:branches:include:- master
    - releases/*
    exclude:- releases/old*

では、リポジトリーで以下のコマンドを打ってみて動作を確認してみましょう。

> git checkout -b release/old/v0.01
> git commit --allow-empty -m "old version"
> git push --set-upstream origin release/old/v0.01

やってみましたがパイプラインは実行されませんでした。

Azure にリソースをデプロイしてみよう

以下のリポジトリの ARM Template をデプロイしてみようと思います。

github.com

今回はちょっとスロットも使いたいなぁと思ったので、スロットの定義を上記のリポジトリのJSONに追加して以下のようにしてみました。

{"$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {"webAppName": {"type": "string",
            "metadata": {"description": "Base name of the resource such as web app name and app service plan"
            },
            "minLength": 2},
        "sku": {"type": "string",
            "defaultValue": "S1",
            "metadata": {"description": "The SKU of App Service Plan, by default is Standard S1"
            }},
        "location": {"type": "string",
            "defaultValue": "[resourceGroup().location]",
            "metadata": {"description": "Location for all resources"
            }}},
    "variables": {"webAppPortalName": "[concat(parameters('webAppName'), '-webapp')]",
        "appServicePlanName": "[concat('AppServicePlan-', parameters('webAppName'))]"
    },
    "resources": [{"apiVersion": "2018-02-01",
            "type": "Microsoft.Web/serverfarms",
            "kind": "app",
            "name": "[variables('appServicePlanName')]",
            "location": "[parameters('location')]",
            "properties": {},
            "dependsOn": [],
            "sku": {"name": "[parameters('sku')]"
            }},
        {"apiVersion": "2018-11-01",
            "type": "Microsoft.Web/sites",
            "kind": "app",
            "name": "[variables('webAppPortalName')]",
            "location": "[parameters('location')]",
            "properties": {"serverFarmId": "[resourceId('Microsoft.Web/serverfarms', variables('appServicePlanName'))]"
            },
            "dependsOn": ["[resourceId('Microsoft.Web/serverfarms', variables('appServicePlanName'))]"
            ]},
        {"apiVersion": "2016-08-01",
            "type": "Microsoft.Web/sites/slots",
            "name": "[concat(variables('webAppPortalName'), '/staging')]",
            "kind": "app",
            "location": "[parameters('location')]",
            "comments": "This specifies the web app slots.",
            "tags": {"displayName": "WebAppSlots"
            },
            "properties": {"serverFarmId": "[resourceId('Microsoft.Web/serverfarms', variables('appServicePlanName'))]"
            },
            "dependsOn": ["[resourceId('Microsoft.Web/Sites', variables('webAppPortalName'))]"
            ]}]}

余談ですが ARM Template Viewer 拡張機能を入れると Visual Studio Code で、こんな感じで見えます。便利。

f:id:okazuki:20200223183607p:plain

パラメーターは以下のような感じで

{"$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {"webAppName": {"value": "devopstestokazuki"
        }}}

出来たのでデプロイしていきます。ドキュメントは以下のページを参考にしました。

github.com

まず、デプロイ先の Azure との接続を作ります。Azure DevOps の左下にある歯車アイコンをクリックして Service connectionsを選択します。

f:id:okazuki:20200223184524p:plain

Azure Resource Managerを選択して Service principal (manual)を選びます。通常は推奨の Service principal (automatic)でいいと思いますが、今回は別テナントの Azure にデプロイしたかったのでマニュアルで各項目を設定しました。

f:id:okazuki:20200223192405p:plain

ここに入力する情報の取得方法は、以下のブログ記事がわかりやすいと思います。

poke-dev.hatenablog.com

Service connections に無事追加されました。

f:id:okazuki:20200223192513p:plain

そして、azure-pipeline.yamlに ARM テンプレートをデプロイするタスクを追加します。

# Starter pipeline# Start with a minimal pipeline that you can customize to build and deploy your code.# Add steps that build, run tests, deploy, and more:# https://aka.ms/yamltrigger:branches:include:- master
    - releases/*
    exclude:- releases/old*

pool:vmImage:'windows-latest'steps:- task: AzureResourceManagerTemplateDeployment@3
    displayName: Deploy ARM template to Azure
    inputs:deploymentScope:'Resource Group'azureResourceManagerConnection:'KazukiOta1'subscriptionId:'ここにデプロイ先のサブスクリプションID'action:'Create Or Update Resource Group'resourceGroupName:'DevOpsTest-rg'location:'Japan East'templateLocation:'Linked artifact'csmFile:'azuredeploy.json'csmParametersFile:'azuredeploy.parameters.json'deploymentMode:'Incremental'

では、これをコミットしてパイプラインを実行してみましょう。

うまくいきました!!

f:id:okazuki:20200223194329p:plain

Azure ポータルを見てみると、ちゃんとリソースも出来てますね。

f:id:okazuki:20200223194419p:plain

アプリのビルドもしてみよう

ということで、アプリもビルドしてみましょう。リポジトリに Visual Studio 2019 で ASP.NET Core MVC のアプリケーションを作ります。ついでに MSTest の単体テストプロジェクトも作ります。こんな感じで。

f:id:okazuki:20200223195049p:plain

適当にテストコードも作っておきました。デフォルトの ErrorViewModel クラスの ShowRequestId プロパティにちょっとしたロジック(文字が空かどうかの判別)が入ってるので、それをチェックする感じにしました。

using HelloPipelineWorldApp.Models;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace HelloPipelineWorldApp.Tests.Models
{
    [TestClass]
    publicclass ErrorViewModelTest
    {
        private ErrorViewModel target;

        [TestInitialize]
        publicvoid Setup()
        {
            target = new ErrorViewModel();
        }

        [TestCleanup]
        publicvoid TearDown()
        {
            target = null;
        }

        [TestMethod]
        publicvoid ShowRequestId_TrueCases()
        {
            target.RequestId = "xxxxx";
            Assert.IsTrue(target.ShowRequestId);
        }

        [TestMethod]
        publicvoid ShowRequestId_FalseCases()
        {
            target.RequestId = null;
            Assert.IsFalse(target.ShowRequestId);

            target.RequestId = "";
            Assert.IsFalse(target.ShowRequestId);
        }
    }
}

では、パイプラインを作っていきます。なんとなく Azure へのリソースのデプロイとアプリのビルドを同じ steps に書くのが嫌だったので、もう一段階上のグルーピングの概念の job を追加してみました。job に分割することでインフラ関連のタスク、アプリをビルドする関連のタスク、その他にアプリをデプロイするタスクを、それぞれ別々にグルーピングすることが出来るようになります。

というわけでさくっと各種タスクを追加しました。

# Starter pipeline# Start with a minimal pipeline that you can customize to build and deploy your code.# Add steps that build, run tests, deploy, and more:# https://aka.ms/yamltrigger:branches:include:- master
    - releases/*
    exclude:- releases/old*

pool:vmImage:'windows-latest'variables:configuration: Release

jobs:- job: infrastructure
    displayName: Create Azure resources
    steps:- task: AzureResourceManagerTemplateDeployment@3
        displayName: Deploy ARM template to Azure
        inputs:deploymentScope:'Resource Group'azureResourceManagerConnection:'KazukiOta1'subscriptionId:'ここにデプロイ先のサブスクリプションID'action:'Create Or Update Resource Group'resourceGroupName:'DevOpsTest-rg'location:'Japan East'templateLocation:'Linked artifact'csmFile:'azuredeploy.json'csmParametersFile:'azuredeploy.parameters.json'deploymentMode:'Incremental'- job: build
    displayName: build and test app
    dependsOn: infrastructure
    condition: succeeded('infrastructure')
    steps:- task: DotNetCoreCLI@2
        displayName: compile
        inputs:command:'build'configuration: $(configuration)
          projects:'**/*.csproj'- task: DotNetCoreCLI@2
        inputs:command:'test'configuration: $(configuration)
          projects:'**/*.Tests.csproj'- task: DotNetCoreCLI@2
        displayName: test
        inputs:command:'test'configuration: $(configuration)
          projects:'**/*.Tests.csproj'- task: DotNetCoreCLI@2
        displayName: publish
        inputs:command:'publish'projects: |
            'HelloPipelineWorldApp/HelloPipelineWorldApp/HelloPipelineWorldApp.csproj''HelloPipelineWorldApp/HelloPipelineWorldApp.sln'publishWebProjects:trueconfiguration: $(configuration)
          modifyOutputPath:truearguments:'-o webapp'- task: PublishBuildArtifacts@1
        displayName: Copy outputs
        inputs:PathtoPublish: webapp
          artifactName: webapp

ジョブに分けたことでパイプラインの実行結果もジョブ単位で見たり出来ます。

f:id:okazuki:20200223201853p:plain

単体テストをテストするタスクも追加したので、Tests タブが追加されています。これをクリックすると単体テストのレポートが見れます。

f:id:okazuki:20200223201939p:plain

もちろん単体テストが失敗すると、パイプライン全体として失敗になります。健全。

f:id:okazuki:20200223202400p:plain

デプロイしよう

アプリがビルドできたら次はデプロイですね。これもデプロイ用のタスクがあるのでさくっと追加してみましょう。

# Starter pipeline# Start with a minimal pipeline that you can customize to build and deploy your code.# Add steps that build, run tests, deploy, and more:# https://aka.ms/yamltrigger:branches:include:- master
    - releases/*
    exclude:- releases/old*

pool:vmImage:'windows-latest'variables:configuration: Release

jobs:- job: infrastructure
    displayName: Create Azure resources
    steps:- task: AzureResourceManagerTemplateDeployment@3
        displayName: Deploy ARM template to Azure
        inputs:deploymentScope:'Resource Group'azureResourceManagerConnection:'KazukiOta1'subscriptionId:'ここにデプロイ先のサブスクリプションID'action:'Create Or Update Resource Group'resourceGroupName:'DevOpsTest-rg'location:'Japan East'templateLocation:'Linked artifact'csmFile:'azuredeploy.json'csmParametersFile:'azuredeploy.parameters.json'deploymentMode:'Incremental'- job: build
    displayName: build and test app
    dependsOn: infrastructure
    condition: succeeded('infrastructure')
    steps:- task: DotNetCoreCLI@2
        displayName: compile
        inputs:command:'build'configuration: $(configuration)
          projects:'**/*.csproj'- task: DotNetCoreCLI@2
        displayName: test
        inputs:command:'test'configuration: $(configuration)
          projects:'**/*.Tests.csproj'- task: DotNetCoreCLI@2
        displayName: test
        inputs:command:'test'configuration: $(configuration)
          projects:'**/*.Tests.csproj'- task: DotNetCoreCLI@2
        displayName: publish
        inputs:command:'publish'projects: |
            'HelloPipelineWorldApp/HelloPipelineWorldApp/HelloPipelineWorldApp.csproj''HelloPipelineWorldApp/HelloPipelineWorldApp.sln'publishWebProjects:trueconfiguration: $(configuration)
          modifyOutputPath:truearguments:'-o webapp'- task: PublishBuildArtifacts@1
        displayName: Copy outputs
        inputs:PathtoPublish: webapp
          artifactName: webapp
  - deployment: deploy
    displayName: Deploy to staging environment and swap
    environment:name: stating
    dependsOn: build
    condition: succeeded('build')
    strategy:runOnce:deploy:steps:- task: AzureWebApp@1
              inputs:azureSubscription:'KazukiOta1'appType:'webApp'appName:'devopstestokazuki-webapp'deployToSlotOrASE:trueresourceGroupName:'DevOpsTest-rg'slotName:'staging'package:'$(Pipeline.Workspace)/webapp/**/*.zip'deploymentMethod:'runFromPackage'- task: AzureAppServiceManage@0
              inputs:azureSubscription:'KazukiOta1'Action:'Swap Slots'WebAppName:'devopstestokazuki-webapp'ResourceGroupName:'DevOpsTest-rg'SourceSlot:'staging'

結構長くなってきましたね…

master ブランチに push するとちゃんと本番までデプロイされてました。

f:id:okazuki:20200223205409p:plain

本当はスワップする前に Selenium とかでの E2E テストとかも走るといいのかもしれないですね。

さて、更新の確認のため Welcome の文字列を Welcome v2 になるように編集して…

f:id:okazuki:20200223205502p:plain

コミットして push してみましょう。しばらく待ってページを確認すると…

f:id:okazuki:20200223210407p:plain

ばっちり!!ステージングの方を見ると古いページが表示されました。これもばっちり。

人による承認をトリガーにしたい

ドキュメント的にはここらへん。

docs.microsoft.com

ステージングにデプロイしてからプロダクションとスワップする前に人の手による承認がしたい!っていうのは世の常ですよね。やってみましょう。 そのために、今はデプロイとスワップが同じ deployment タスクになっていたのを別々にしたいと思います。

# Starter pipeline# Start with a minimal pipeline that you can customize to build and deploy your code.# Add steps that build, run tests, deploy, and more:# https://aka.ms/yamltrigger:branches:include:- master
    - releases/*
    exclude:- releases/old*

pool:vmImage:'windows-latest'variables:configuration: Release

jobs:- job: infrastructure
    displayName: Create Azure resources
    steps:- task: AzureResourceManagerTemplateDeployment@3
        displayName: Deploy ARM template to Azure
        inputs:deploymentScope:'Resource Group'azureResourceManagerConnection:'KazukiOta1'subscriptionId:'ここにデプロイ先のサブスクリプションID'action:'Create Or Update Resource Group'resourceGroupName:'DevOpsTest-rg'location:'Japan East'templateLocation:'Linked artifact'csmFile:'azuredeploy.json'csmParametersFile:'azuredeploy.parameters.json'deploymentMode:'Incremental'- job: build
    displayName: build and test app
    dependsOn: infrastructure
    condition: succeeded('infrastructure')
    steps:- task: DotNetCoreCLI@2
        displayName: compile
        inputs:command:'build'configuration: $(configuration)
          projects:'**/*.csproj'- task: DotNetCoreCLI@2
        displayName: test
        inputs:command:'test'configuration: $(configuration)
          projects:'**/*.Tests.csproj'- task: DotNetCoreCLI@2
        displayName: test
        inputs:command:'test'configuration: $(configuration)
          projects:'**/*.Tests.csproj'- task: DotNetCoreCLI@2
        displayName: publish
        inputs:command:'publish'projects: |
            'HelloPipelineWorldApp/HelloPipelineWorldApp/HelloPipelineWorldApp.csproj''HelloPipelineWorldApp/HelloPipelineWorldApp.sln'publishWebProjects:trueconfiguration: $(configuration)
          modifyOutputPath:truearguments:'-o webapp'- task: PublishBuildArtifacts@1
        displayName: Copy outputs
        inputs:PathtoPublish: webapp
          artifactName: webapp
  - deployment: deploy_to_staging_environment
    displayName: Deploy to staging environment
    environment:name: stating
    dependsOn: build
    condition: succeeded('build')
    strategy:runOnce:deploy:steps:- task: AzureWebApp@1
              inputs:azureSubscription:'KazukiOta1'appType:'webApp'appName:'devopstestokazuki-webapp'deployToSlotOrASE:trueresourceGroupName:'DevOpsTest-rg'slotName:'staging'package:'$(Pipeline.Workspace)/webapp/**/*.zip'deploymentMethod:'runFromPackage'- deployment: deploy_to_production_environment
    displayName: Deploy to production environment
    environment:name: production
    dependsOn: deploy_to_staging_environment
    condition: succeeded('deploy-to-staging-environment')
    strategy:runOnce:deploy:steps:- task: AzureAppServiceManage@0
              inputs:azureSubscription:'KazukiOta1'Action:'Swap Slots'WebAppName:'devopstestokazuki-webapp'ResourceGroupName:'DevOpsTest-rg'SourceSlot:'staging'

そして、Azure Pipelines の Environments に行きます。

f:id:okazuki:20200223211604p:plain

staging と production を New environment ボタンから作ります。ここまでの手順を追ってきた人は多分 staging は作られていると思います。yaml の deployment タスクで指定した environment がここに出てきます。今回は production に対して手動認証を追加します。

f:id:okazuki:20200223211647p:plain

production を選択して画面右上の縦に点が 3 つ並んだボタンを押して Approvals and checks を選択します。

f:id:okazuki:20200223211837p:plain

画面右上の +ボタンを押すと以下のような画面が出るので Approvals を選択してる状態で Next を選択。

f:id:okazuki:20200223211927p:plain

誰に承認権限を与えるかなどを設定して Create を押しましょう。

f:id:okazuki:20200223212012p:plain

では、v3 になるようにソースコードを変更して commit & push します。 そして、これではだめでした。パイプライン全体がペンディングになってしまいました。

f:id:okazuki:20200223214134p:plain

マニュアルでの承認を特定の Environment に適用すると、その Environment に影響を与える可能性のあるパイプラインの実行自体が承認があるまで停止するみたいですね。

なので、今回のケースでは production 環境への変更時のみ人の手による認証がしたいので、該当部分の swap 部分を別のパイプラインとして作成して、ビルドとステージング環境へのデプロイのパイプラインが完了したタイミングで実行するように構成すればよいということになります。

なので、azure-pipelines.ymlから swap 部分を取り除いて以下のようにします。

# Starter pipeline# Start with a minimal pipeline that you can customize to build and deploy your code.# Add steps that build, run tests, deploy, and more:# https://aka.ms/yamltrigger:branches:include:- master
    - releases/*
    exclude:- releases/old*

pool:vmImage:'windows-latest'variables:configuration: Release

jobs:- job: infrastructure
    displayName: Create Azure resources
    steps:- task: AzureResourceManagerTemplateDeployment@3
        displayName: Deploy ARM template to Azure
        inputs:deploymentScope:'Resource Group'azureResourceManagerConnection:'KazukiOta1'subscriptionId:'ここにデプロイ先のサブスクリプションID'action:'Create Or Update Resource Group'resourceGroupName:'DevOpsTest-rg'location:'Japan East'templateLocation:'Linked artifact'csmFile:'azuredeploy.json'csmParametersFile:'azuredeploy.parameters.json'deploymentMode:'Incremental'- job: build
    displayName: build and test app
    dependsOn: infrastructure
    condition: succeeded('infrastructure')
    steps:- task: DotNetCoreCLI@2
        displayName: compile
        inputs:command:'build'configuration: $(configuration)
          projects:'**/*.csproj'- task: DotNetCoreCLI@2
        displayName: test
        inputs:command:'test'configuration: $(configuration)
          projects:'**/*.Tests.csproj'- task: DotNetCoreCLI@2
        displayName: test
        inputs:command:'test'configuration: $(configuration)
          projects:'**/*.Tests.csproj'- task: DotNetCoreCLI@2
        displayName: publish
        inputs:command:'publish'projects: |
            'HelloPipelineWorldApp/HelloPipelineWorldApp/HelloPipelineWorldApp.csproj''HelloPipelineWorldApp/HelloPipelineWorldApp.sln'publishWebProjects:trueconfiguration: $(configuration)
          modifyOutputPath:truearguments:'-o webapp'- task: PublishBuildArtifacts@1
        displayName: Copy outputs
        inputs:PathtoPublish: webapp
          artifactName: webapp
  - deployment: deploy_to_staging_environment
    displayName: Deploy to staging environment
    environment:name: stating
    dependsOn: build
    condition: succeeded('build')
    strategy:runOnce:deploy:steps:- task: AzureWebApp@1
              inputs:azureSubscription:'KazukiOta1'appType:'webApp'appName:'devopstestokazuki-webapp'deployToSlotOrASE:trueresourceGroupName:'DevOpsTest-rg'slotName:'staging'package:'$(Pipeline.Workspace)/webapp/**/*.zip'deploymentMethod:'runFromPackage'

そして、Azure DevOps のパイプラインのページから、もう1つパイプラインを追加します。そして、中身を以下のようにしてデプロイ部分だけを移植します。このとき trigger を none にして特定ブランチへの push などでは動かないようにして resources で AzurePipelineLab パイプラインをきっかけに動くようにします。

trigger: none
resources:pipelines:- pipeline: build_and_deploy_to_staging
    source: AzurePipelineLab
    trigger:branches:- master

pool:vmImage:'windows-latest'jobs:- deployment: deploy_to_production_environment
    displayName: Deploy to production environment
    environment:name: production
    strategy:runOnce:deploy:steps:- task: AzureAppServiceManage@0
              inputs:azureSubscription:'KazukiOta1'Action:'Swap Slots'WebAppName:'devopstestokazuki-webapp'ResourceGroupName:'DevOpsTest-rg'SourceSlot:'staging'

こんな感じにします。

master ブランチに push すると Swap 用のパイプラインがずっとペンディング状態になります。

f:id:okazuki:20200223231422p:plain

ペンディング状態のものを見てみると承認が必要と表示されてますね。

f:id:okazuki:20200223231523p:plain

アプルーブ!!

f:id:okazuki:20200223231642p:plain

ちゃんとアプルーブすると本番が v3 になってステージングが v2 になりました。入れ替わったね。

f:id:okazuki:20200223232134p:plain

まとめ

ということで environment と pipeline をトリガーにして複数パイプラインを連携させることで昔の Release pipeline でやってたような手動承認が出来るようになりました。

Azure DevOps のパイプラインのテンプレートを使ってみた

$
0
0

昨日は、べたっとパイプラインを書きました。

blog.okazuki.jp

そして、横浜さんのブログを読んでるとテンプレートなるものが出てきてますね。

blog.beachside.dev

ドキュメントとしては以下の部分ですね。

docs.microsoft.com

テンプレート

パイプラインを小さな単位に分割して、それを別のファイルから参照するような仕組みですね。大きなパイプラインを分割して見通しをよくしたり、複数個のパイプラインで同じような処理をするときに再利用可能な形で使うことが出来そうです。

ではやってみましょう。

Hello world

何事もハローワールドから。テンプレートの書きかたは、parameters というものに外部から渡したいパラメーターを定義して、その上で中身を書いていく感じ。適当なリポジトリーを Azure Repos 上に作って templates/helloworld-template.yml という名前で以下のようなファイルを作りました。

parameters:- name: message
  type: string
  default: Hello world

steps:- script: echo ${{ parameters.message }}

これで message というパラメーターを受け取って、それを echo で出力するテンプレートが出来ました。では、実際にパイプラインから使ってみましょう。適当にパイプラインを作って以下のような内容にします。テンプレートを読み込むには steps や jobs とかで template で指定します。横浜さんのブログによると、パイプラインのあるフォルダーからの相対パスでテンプレートを指定するみたいです。ルートからのパスじゃないということに気を付けないとですね。

trigger: none

pool:vmImage:'ubuntu-latest'steps:- template: templates/helloworld-template.yml

実行すると Hello world が出ました。

f:id:okazuki:20200224095148p:plain

パラメーターを渡す場合は - template:に続けて以下のように parametersを追加すれば OK です。

trigger: none

pool:vmImage:'ubuntu-latest'steps:- template: templates/helloworld-template.yml
  parameters:message:'こんにちは世界'

実行すると以下のように、ちゃんと「Hello world」だったものが「こんにちは世界」になってますね。

f:id:okazuki:20200224095557p:plain

変数とかも別ファイルに外だしとかできますね。

# templates/variables-template.ymlvariables:- name: message
  value: Hello world


# azure-pipeline.ymltrigger: none

variables:- template: templates/variables-template.yml

steps:- script: echo $(message)

上記の例では使ってませんが、ちゃんとこの場合でも parameters が使えます。

もうちょっとドキュメントを参照してみると、以下のように stepList とかみたいに step のリストもパラメーターとして渡すことができるようになってます。

f:id:okazuki:20200224101916p:plain

試してみましょう。templates/steps-template.ymlという名前で以下のようなファイルを作ってみました。

parameters:- name: steps
  type: stepList
  default:[]steps:- script: echo pre-process
  displayName: pre-process
- ${{ each step in parameters.steps }}:
  - ${{ step }}
- script: echo post-process
  displayName: post-process

そして、azure-pipeline.ymlを変更して上のテンプレートを使うようにしました。

trigger: none

pool:vmImage:'ubuntu-latest'steps:- template: templates/steps-template.yml
  parameters:steps:- script: echo Hello world
      displayName: process for English
    - script: echo こんにちは世界
      displayName: process for Japanese

テンプレートに steps という名前のパラメーターを渡していて、そこで echo を 2 つ実行しています。テンプレートでは pre-process を実行してテンプレートで渡されたステップを展開して、 post-process という流れにしてます。実行してみると…

f:id:okazuki:20200224102241p:plain

ちゃんと、思った通りに pre-process → パラメーターで渡したステップ → post-process の順番んで実行されてますね!

ちなみに、今回のようにパイプラインのフローを別のテンプレートで定義して、それをメインのパイプラインから参照する場合は extends を使って以下のようにも書けます。

trigger: none

extends:template: templates/steps-template.yml
  parameters:steps:- script: echo Hello world
      displayName: process for English
    - script: echo こんにちは世界
      displayName: process for Japanese

この場合 pool ってどうやって定義するんだろう?という感じですが、テンプレート側に job 定義すればいけそうですね。ということでテンプレートのほうを以下のようにして windows-latest を指定してみました。

parameters:- name: steps
  type: stepList
  default:[]jobs:- job:pool:vmImage:'windows-latest'steps:- script: echo pre-process
      displayName: pre-process
    - ${{ each step in parameters.steps }}:
      - ${{ step }}
    - script: echo post-process
      displayName: post-process

これで実行すると、ちゃんと windows-latest になってますね。

f:id:okazuki:20200224103850p:plain

まとめ

ということで、これでテンプレートの再利用が捗りそうです。

Azure DevOps の Pipelines の変数を使おう

$
0
0

ハードコーディングされた値は死すべし!! ということで Azure DevOps の Pipelines で変数使っていこうと思います。

docs.microsoft.com

ハローワールド

何事もハローワルドから。variables で変数を定義できます。定義した変数は $(変数名)で参照できます。ということでさくっと以下のようはパイプラインの yaml を作ってみました。後々の確認のために特に必要はないのですが stage から定義しています。

trigger:- master

variables:var1: Hello
  var2: World

pool:vmImage:'ubuntu-latest'stages:- stage: build
    displayName: build
    jobs:- job: test1
      steps:- pwsh: |
          Write-Host '$(var1)'
          Write-Host '$(var2)'

pwshタスクで定義した変数を参照しています。このパイプラインが実行されると以下のように表示されます。

Hello
World

いい感じ。

その他にも ${{ xxx }}のように書いて実行時じゃなくてコンパイル時に変数を評価するような書き方や $[xxx]のように書く方法もあります。$[xxx]という書き方も実行時に評価されるらしいですが、これは pwsh とかの中には書けませんでした。

trigger:- master

variables:var1: Hello
  var2: World

pool:vmImage:'ubuntu-latest'stages:- stage: build
    displayName: build
    jobs:- job: test1
      variables:jobVar1: $[variables.var1]
        jobVar2: $[variables.var2]
      steps:- pwsh: |
          Write-Host '$(var1)'
          Write-Host '$(var2)'- pwsh: |
          Write-Host '${{variables.var1}}'
          Write-Host '${{variables.var2}}'- pwsh: |
          Write-Host '$(jobVar1)'
          Write-Host '$(jobVar2)'

しれっと使ってますが job レベルとかでも変数を定義できます。上の yaml でいう jobVar1jobVar2を定義しているところですね。

スクリプトの中で変数を定義したい

pwsh タスクなどの中で特別なフォーマットで標準出力に書き込むことでスクリプト内で変数を定義できます。出力する内容は "##vso[task.setvariable variable=変数名]値"です。これで変数を定義できます。やってみましょう。

trigger:- master

variables:var1: Hello
  var2: World

pool:vmImage:'ubuntu-latest'stages:- stage: build
    displayName: build
    jobs:- job: test1
      steps:- pwsh: |
          Write-Host '##vso[task.setvariable variable=fromScript]Hello from Script'- pwsh: |
          Write-Host '$(fromScript)'

これで2つ目の pwsh のほうで Hello from Scriptが表示されます。

同じ job 内の task からなら $(変数名)でいけるのですが、これが job を跨ぐと参照できなくなります。試しにこうしてみましょう。

trigger:- master

variables:var1: Hello
  var2: World

pool:vmImage:'ubuntu-latest'stages:- stage: build
    displayName: build
    jobs:- job: test1
      steps:- pwsh: |
          Write-Host '##vso[task.setvariable variable=fromScript]Hello from Script'- job: test2
      dependsOn: test1
      steps:- pwsh: |
          Write-Host '$(fromScript)'

これを実行すると悲しいことに $(fromScript)が表示されます。これは定義されていない変数を参照したとき表示されるやつですね。job を跨いだ変数の参照には一工夫がいります。

まず、script 内で変数を定義するときに isOutput=trueを定義して出力に含めるようにします。こうすることで job の出力として別の job から参照できます。参照する側では $[dependencies.依存先job名.outputs['タスク名.変数名']]のようになります。あと、dependencies として参照するので dependsOn にも依存先として明示しておきます。以下のような感じです。

trigger:- master

variables:var1: Hello
  var2: World

pool:vmImage:'ubuntu-latest'stages:- stage: build
    displayName: build
    jobs:- job: test1
      steps:- pwsh: |
          Write-Host '##vso[task.setvariable variable=fromScript;isOutput=true]Hello from Script'name: scriptTask
    - job: test2
      dependsOn: test1
      variables:fromScriptInTest1: $[dependencies.test1.outputs['scriptTask.fromScript']]
      steps:- pwsh: |
          Write-Host '$(fromScriptInTest1)'

これで、fromScriptInTest1 変数に test1 ジョブで定義した fromScript 変数が入ります。

このスクリプト内での変数の宣言を応用すると、ARM Template の outputs を変数として定義することができるようになります。以下のような感じでいけます。

stages:- stage: build_and_deploy
    displayName: Build and deploy
    jobs:- job: infrastructure
        displayName: Create Azure resources
        steps:- task: AzureResourceManagerTemplateDeployment@3
            displayName: Deploy ARM template to Azure
            inputs:deploymentScope:'Resource Group'azureResourceManagerConnection:'KazukiOta1'subscriptionId:'xxxxxx-xxxxx-xxxxx-xxxxxx'action:'Create Or Update Resource Group'resourceGroupName: $(resourceGroupName)
              location:'Japan East'templateLocation:'Linked artifact'csmFile:'azuredeploy.json'csmParametersFile:'azuredeploy.parameters.json'deploymentMode:'Incremental'deploymentOutputs: armOutputs
          - pwsh: |
              $outputs = ConvertFrom-Json '$(armOutputs)'
              foreach ($x in $outputs.PSObject.Properties) {
                Write-Host "##vso[task.setvariable variable=armOutputs_$($x.Name);isSecure=true;isOutput=true]$($x.Value.value)"}

deploymentOutputs で ARM Template の出力の JSON を受け取って PowerShell 内でパースして ##vso...を使って変数にしています。

ステージ間の変数の参照

これは正攻法ではできないみたいなので頑張るしかないみたいです。

以下の medium に、アーティファクトに変数の内容を出力しておいて、別のステージではアーティファクトをダウンロードして読み込んで使うという方法が書いてありました。

medium.com

ちょっと大変ですね。

variable group

Pipelines の Library の中に Variable Group というものが作れます。ここで以下のように変数のグループを定義できます。

f:id:okazuki:20200301235410p:plain

もう 1 つグループ作りました。こっちは日本語

f:id:okazuki:20200301235904p:plain

このようにしておくと、以下のように stage 単位や job 単位でさくっと変数を切り替えれます。

trigger:- master

pool:vmImage:'ubuntu-latest'stages:- stage: development
    variables:- group: vargroup1
    - name: var2
      value: localVar
    jobs:- job: test1
      steps:- pwsh: |
          Write-Host '$(message)'
          Write-Host '$(var2)'- stage: production
    variables:- group: vargroup2
    - name: var2
      value:'ローカル変数'jobs:- job: test1
      steps:- pwsh: |
            Write-Host '$(message)'
            Write-Host '$(var2)'

これを実行すると英語と日本語でそれぞれでますね。ばっちり。

f:id:okazuki:20200302000521j:plain

f:id:okazuki:20200302000534j:plain

template

template を使って variables を yaml として定義しておいて取り込むことで同じようなこともできます。 例えば vartemplate1.yml を以下の内容で作って

variables:message: Hello world

vartemplate2.yml を以下の内容で作って

variables:message:'こんにちは世界'

そして、azurepipeline.yml を group から template に置き換えます。

trigger:- master

pool:vmImage:'ubuntu-latest'stages:- stage: development
    variables:- template: vartemplate1.yml
    - name: var2
      value: localVar
    jobs:- job: test1
      steps:- pwsh: |
          Write-Host '$(message)'
          Write-Host '$(var2)'- stage: production
    variables:- template: vartemplate2.yml
    - name: var2
      value:'ローカル変数'jobs:- job: test1
      steps:- pwsh: |
            Write-Host '$(message)'
            Write-Host '$(var2)'

これでも Variable group と同じ結果が得られます。違いとしては、template の方はリポジトリーに入るけど Variable group はリポジトリには入らないという点です。リポジトリに入ったらまずい情報とかをまとめて置いておきたいとか時に気軽に使えます。

秘密な情報

その他にパイプラインの画面から変数を定義できます。こちらは、もうちょっと秘密なものを置けそう。

f:id:okazuki:20200302001546j:plain

f:id:okazuki:20200302001600p:plain

さらに、もっと秘密な情報を保存しておきたい時は Variable Group に Azure KeyVault との連携もあります。(これは自分ではまだ動作確認してない)

docs.microsoft.com

ということで秘密なものをきちんと全体で管理したいなら Azure KeyVault で、そうじゃないならパイプラインの変数でという感じでしょうか。もうちょっとゆるいやつは Variable group に限定的な人しか読めないようにしておいておくとか?

まとめ

ということで、Azure DevOps の Pipelines ネタのブログなら iPad で書けるのではないかと思って書いてみました。意外と画像編集もできるし workingcopy という git クライアント買えば Azure DevOps から clone してローカルで Textastic で編集して push とかも出来ていい感じでした。

でも、パソコンの方がやっぱりこういう作業は今のところ快適でした。

SQL Database の SKU のリストが欲しい

$
0
0

以下のような Azure CLI のコマンドでとれる。

az sql db list-editions -l japaneast

結果がずらららら~~~っと出てくる。もうちょっと名前だけに絞りたいときは以下のような PowerShell のワンライナーでいけそう。

az sql db list-editions -l japaneast | ConvertFrom-Json | foreach { $_.supportedServiceLevelObjectives | foreach { $_.name } }

System とかいらなさそうなのも入ってるので GeneralPurpose のみに絞るなら…

az sql db list-editions -l japaneast | ConvertFrom-Json | where { $_.name -eq "GeneralPurpose" } | foreach { $_.supportedServiceLevelObjectives | foreach { $_.name } }

とかかな?結果はこんな感じ。

GP_Gen4_1
GP_S_Gen5_1
GP_Gen4_2
GP_Gen5_2
GP_S_Gen5_2
GP_Gen4_3
GP_Gen4_4
GP_Gen5_4
GP_S_Gen5_4
GP_Gen4_5
GP_Gen4_6
GP_Gen5_6
GP_S_Gen5_6
GP_Gen4_7
GP_Gen4_8
GP_Gen5_8
GP_S_Gen5_8
GP_Gen4_9
GP_Gen4_10
GP_Gen5_10
GP_S_Gen5_10
GP_Gen5_12
GP_S_Gen5_12
GP_Gen5_14
GP_S_Gen5_14
GP_Gen4_16
GP_Gen5_16
GP_S_Gen5_16
GP_Gen5_18
GP_Gen5_20
GP_Gen4_24
GP_Gen5_24
GP_Gen5_32
GP_Gen5_40
GP_Gen5_80

Azure DevOps の Pipelines の template や variable group で変数名の違いを吸収する

$
0
0

ただの小ネタです。

Azure Pipelines でリポジトリに含めたくない秘密の値を入れるための Variables に変数を追加します。

f:id:okazuki:20200302170819p:plain

そして Variable Group でも template でもいいのですが 2 セット変数の定義を作ります。

vartemplate-for-production.yml

variables:message: $(messageForProduction)

vartemplate-for-staging.yml

variables:message: $(messageForStaging)

適当なタスクも作っておきます。

some-tasks.yml

parameters:- name: vartemplatename
  type: string
  default:''jobs:- job: job1
  variables:- template: ${{ parameters.vartemplatename }}
  steps:- script: echo $(message)

とりあえずテンプレートで本番用とステージング用の変数名の違いを吸収してるから、いい感じにタスクを共通化出来そうという魂胆です。 パラメーターで変数のテンプレートファイル名を渡す感じですね。なのでメインのパイプラインはこんな感じ。

trigger:- master

pool:vmImage:'ubuntu-latest'stages:- stage: staging
    jobs:- template: some-tasks.yml
      parameters:vartemplatename: vartemplate-for-staging.yml

  - stage: production
    jobs:- template: some-tasks.yml
      parameters:vartemplatename: vartemplate-for-production.yml

実行すると、それぞれの環境用の内容が表示されます。

Azure App Configuration を試してみよう on Azure Functions

$
0
0

アプリ設定を WebApps の構成のアプリケーション設定とは別に管理できる App Configuration を試してみました!

docs.microsoft.com

最近は ARM Template 書いてみるのがブームなので、以下のように ARM Template 作って下準備しました。

{"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {"appName": {"type": "string"
        }},
    "variables": {"storageAccountName": "[concat(parameters('appName'), 'storage')]",
        "storageAccountid": "[concat(resourceGroup().id,'/providers/','Microsoft.Storage/storageAccounts/', variables('storageAccountName'))]",
        "appInsightsName": "[concat(parameters('appName'), '-insights')]",
        "functionAppName": "[concat(parameters('appName'), '-func')]",
        "hostingPlanName": "[concat(parameters('appName'), '-plan')]",
        "appConfigurationName": "[concat(parameters('appName'), 'config')]",
        "appConfigurationId": "[concat(resourceGroup().id,'/providers/','Microsoft.AppConfiguration/configurationStores/', variables('appConfigurationName'))]"
    },
    "resources": [{"type": "Microsoft.Storage/storageAccounts",
            "name": "[variables('storageAccountName')]",
            "apiVersion": "2019-04-01",
            "location": "[resourceGroup().location]",
            "kind": "StorageV2",
            "sku": {"name": "Standard_LRS"
            }},
        {"apiVersion": "2015-05-01",
            "name": "[variables('appInsightsName')]",
            "type": "Microsoft.Insights/components",
            "kind": "web",
            "location": "[resourceGroup().location]",
            "tags": {"[concat('hidden-link:', resourceGroup().id, '/providers/Microsoft.Web/sites/', variables('functionAppName'))]": "Resource"
            },
            "properties": {"Application_Type": "web",
                "ApplicationId": "[variables('appInsightsName')]"
            }},
        {"type": "Microsoft.Web/serverfarms",
            "apiVersion": "2016-09-01",
            "name": "[variables('hostingPlanName')]",
            "location": "[resourceGroup().location]",
            "properties": {"name": "[variables('hostingPlanName')]",
                "computeMode": "Dynamic"
            },
            "sku": {"name": "Y1",
                "tier": "Dynamic",
                "size": "Y1",
                "family": "Y",
                "capacity": 0}},
        {"apiVersion": "2016-08-01",
            "type": "Microsoft.Web/sites",
            "name": "[variables('functionAppName')]",
            "location": "[resourceGroup().location]",
            "kind": "functionapp",
            "dependsOn": ["[resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName'))]"
            ],
            "properties": {"siteConfig": {"appSettings": [{"name": "AzureWebJobsStorage",
                            "value": "[concat('DefaultEndpointsProtocol=https;AccountName=', variables('storageAccountName'), ';AccountKey=', listKeys(variables('storageAccountid'),'2015-05-01-preview').key1)]"
                        },
                        {"name": "WEBSITE_CONTENTAZUREFILECONNECTIONSTRING",
                            "value": "[concat('DefaultEndpointsProtocol=https;AccountName=', variables('storageAccountName'), ';AccountKey=', listKeys(variables('storageAccountid'),'2015-05-01-preview').key1)]"
                        },
                        {"name": "WEBSITE_CONTENTSHARE",
                            "value": "[toLower(variables('functionAppName'))]"
                        },
                        {"name": "FUNCTIONS_WORKER_RUNTIME",
                            "value": "dotnet"
                        },
                        {"name": "FUNCTIONS_EXTENSION_VERSION",
                            "value": "~3"
                        },
                        {"name": "WEBSITE_RUN_FROM_PACKAGE",
                            "value": 1},
                        {"name": "APPINSIGHTS_INSTRUMENTATIONKEY",
                            "value": "[reference(resourceId('microsoft.insights/components/', variables('appInsightsName')), '2015-05-01').InstrumentationKey]"
                        },
                        {"name": "AppConfigurationConnectionString",
                            "value": "[ListKeys(variables('appConfigurationId'), '2019-10-01').value[2].connectionString]"
                        }]}}},
        {"name": "[variables('appConfigurationName')]",
            "type": "Microsoft.AppConfiguration/configurationStores",
            "apiVersion": "2019-10-01",
            "location": "[resourceGroup().location]",
            "properties": {},
            "sku": {"name": "standard"
            }}],
    "outputs": {},
    "functions": []}

App Configuration の接続文字列は ListKeys 関数でプライマリー読み書き、セカンダリー読み書き、プライマリー読み込み専用、セカンダリー読み込み専用の順番で入った値が返ってくるので 3 番目の要素の connectionString で取れます。

App Configuratoin の ARM Template は以下のドキュメントを参考にしました。

docs.microsoft.com

接続文字列をとってくる関数は、以下のドキュメントを見て ListKeys なんだと確認しました。

docs.microsoft.com

とりあえず App Configuration を使わないケース

Azure Functions のプロジェクトを作って以下の NuGet パッケージを入れます。DIしたいので。

  • Microsoft.Azure.Functions.Extensions

local.settings.json を以下のようにして…

{"IsEncrypted": false,
    "Values": {"AzureWebJobsStorage": "UseDevelopmentStorage=true",
        "FUNCTIONS_WORKER_RUNTIME": "dotnet",
        "Sample:Message": "Hello from local.settings.json"
    }}

そして、Startup.cs で SampleObject というクラスに Sample:Message が設定されるようにします。

using Microsoft.Azure.Functions.Extensions.DependencyInjection;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

[assembly: FunctionsStartup(typeof(AppConfigFunc.Startup))]

namespace AppConfigFunc
{
    publicclass Startup : FunctionsStartup
    {
        private IConfiguration Configuration { get; }

        public Startup()
        {
            var config = new ConfigurationBuilder()
                .AddEnvironmentVariables()
                .AddJsonFile("local.settings.json", true);

            // 後で App Configuration から取り込む予定

            Configuration = config.Build();       
        }

        publicoverridevoid Configure(IFunctionsHostBuilder builder)
        {
            builder.Services.Configure<SampleObject>(Configuration.GetSection("Sample"));
        }
    }

    publicclass SampleObject
    {
        publicstring Message { get; set; }
    }
}

適当に関数で使ってみましょう。SampleObject を DI して Message を返すだけの関数を作ります。

using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Microsoft.Extensions.Options;

namespace AppConfigFunc
{
    publicclass SayHello
    {
        privatereadonly SampleObject _sampleObject;
        public SayHello(IOptions<SampleObject> sampleObject)
        {
            _sampleObject = sampleObject.Value;
        }

        [FunctionName("SayHello")]
        public IActionResult Run(
            [HttpTrigger(AuthorizationLevel.Function, "get", Route = null)] HttpRequest req,
            ILogger log)
        {
            returnnew OkObjectResult(_sampleObject);
        }
    }
}

実行して SayHello 関数を呼び出すと…

f:id:okazuki:20200303215506p:plain

思った通りですね。

App Configuration 対応

では、App Configuration 対応してみます。以下の NuGet パッケージを追加します。

  • Microsoft.Extensions.Configuration.AzureAppConfiguration

Startup.cs を以下のようにして本番のときだけ App Configuration から値を取るようにしてみました。 今回はローカルでは local.settings.json を使う想定で

using Microsoft.Azure.Functions.Extensions.DependencyInjection;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

[assembly: FunctionsStartup(typeof(AppConfigFunc.Startup))]

namespace AppConfigFunc
{
    publicclass Startup : FunctionsStartup
    {
        public IConfiguration Configuration { get; }

        public Startup()
        {
            var config = new ConfigurationBuilder()
                .AddEnvironmentVariables()
                .AddJsonFile("local.settings.json", true);

            // App Configuration から取り込む
            var builtConfig = config.Build();
            if (builtConfig["AZURE_FUNCTIONS_ENVIRONMENT"] != "Development")
            {
                config.AddAzureAppConfiguration(builtConfig["AppConfigurationConnectionString"]);
            }

            Configuration = config.Build();       
        }

        publicoverridevoid Configure(IFunctionsHostBuilder builder)
        {
            builder.Services.Configure<SampleObject>(Configuration.GetSection("Sample"));
        }
    }

    publicclass SampleObject
    {
        publicstring Message { get; set; }
    }
}

コードは安定のしばやんの blog を参考にしました。

blog.shibayan.jp

そして、App Configuration に以下のような値を設定します。

f:id:okazuki:20200303222007p:plain

ARM Template で作った Function Apps にデプロイして実行すると、以下のような値になりました!やったね!

f:id:okazuki:20200303222042p:plain

App Configuration の設定を動的に変更したい

その時用のメモとしてドキュメントへのリンクをぺたり。

docs.microsoft.com

ソースコード

github.com


ASP.NET Core や Azure Functions でインフラを含めた CI/CD をアプリ設定の矛盾なく行う方法

$
0
0

というのを最近考えてて、インフラ設定は ARM Template でアプリ設定は App Configuration にしておいたらいいのかなぁと思ってました。 という考えに至った後にしばやん雑記に以下の記事を発見。

blog.shibayan.jp

あ、一番シンプルな JSON で appsettings.Production.json をリリースビルド時に同梱するのがいいですね!流石。 App Configuration は、その機能が必要なときに使えばいいのは確か。

すっきり。

Cosmos DB がらみの記事を Qiita に 3 つ書いてみた

$
0
0

Qiita が、ここ暫く何か技術的な記事を日本語で書くならデファクトスタンダードっぽいけど、使ったことなかったので使ってみました。 以下の3記事をとりあえず。

qiita.com

qiita.com

qiita.com

暫く Qiita 使ってみます

$
0
0

長らく、このブログを使ってましたが、ちょっと Qiita を使ってみようと思います。 飽きるか、はてなブログのほうが使いやすいな~と思ったら戻ってきます。

ということでフォローよろしくお願いします!

qiita.com

アプリケーション開発経験が、ほぼない開発者のポエム

$
0
0

自分のキャリアは多分結構特殊で、今までユーザーがアプリケーションやサービスとして利用する成果物を作成するプロジェクトに要件定義・設計・プログラミング・テスト・リリース・バージョンアップと参加したプロジェクトは一つだけになります。

それ以外の経験については

仕事

  • 全社横断的な技術支援を行う部署でスポットで入って問題や課題の解決とともに離脱
  • 社内向け Java の Web アプリケーション開発フレームワークの開発・保守・サポート
  • 社内技術情報の標準化・ドキュメント作成
  • 教育
  • パッケージソフトのサポート・機能追加プロジェクトでエクセルとにらめっこしたり、テスト考えたり

趣味

  • この Blog で気に入った技術を試してブログ、試してブログ、試してブログ
  • 勉強会登壇
  • 書籍執筆
  • いいと思ったライブラリが放置されているのを引き取ってメンテナンス

あたりが主なものになります。なので

というツイートに対して別に何かアプリを組まないといけないということはないと思ったので

という引用リツイートをしたら

「教科書を読んだだけ」という条件がどうも暗黙的にあったらしく 4 年弱の時を超えて引用リーツイートされました\o^/

教科書読んだだけでプログラム書いてない人が「出来ます!」って言ってきたら本当かなぁ…って思いますよ…。

まとめ

技術記事は Qiita に引っ越したのでブログに心置きなくポエムや愚痴が書けるね! 流石に Qiita で運営にポエムと判定されるかどうかチャレンジは怖いので出来ない。

自動で Windwos 10 の壁紙とアクセント カラーを変えて気分転換

$
0
0

自宅でずっと仕事をしてると毎日が同じ風景で気分転換もしづらくて何となくストレスを感じるので、少しでも気分転換になるように壁紙の自動変更とアクセント カラーの自動変更の設定をしました。

といっても、壁紙の自動変更は自前の画像だけでは限界があるので Bing や Windows spotlight からとってきて自動で壁紙を変えてくれる Dynamic Theme というアプリを入れました。

www.microsoft.com

このアプリを起動すると壁紙とロックスクリーンの壁紙を Bing や Windows spotlight とかから取得するように設定できます。

f:id:okazuki:20200423140701p:plain

Bing の画像は、綺麗でかつ無難(重要)な画像が毎日更新されていくので、とてもいい感じです。 以下のような画像が日替わりできます。

f:id:okazuki:20200423140834p:plain

f:id:okazuki:20200423140934p:plain

f:id:okazuki:20200423140949p:plain

壁紙が自動でいい感じの画像になるようになったので仕上げです。Windows の設定アプリで「個人用設定」の「色」を選んでアクセント カラーを選ぶの所で「背景から自動的にアクセント カラーを選ぶ」にチェックを入れます。 これでアクセント カラーが背景画像から変わるようになります。自動で壁紙が変わるように設定したので、自動的にアクセント カラーも変わります。

悲しいのはアクセント カラーが反映されるアプリケーションは UWP アプリが多いのですが、純粋な UWP アプリの数が少ないので辛い…。 自分が使う頻度の高いアプリだと標準のメール・カレンダーアプリがアクセント カラーに対応してるので、これで体感してます。(電卓は目隠し用)

f:id:okazuki:20200423142208p:plain

まとめ

ということで、自分が毎日同じ風景になる問題を少しだけ軽減する方法として設定している Windows の設定を紹介しました。 まぁ、毎日散歩するのが一番なんですけどね。

Viewing all 1388 articles
Browse latest View live


<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>