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

Marp 使う時に見るもの


Marp を使ってスライドを作ってみる

$
0
0

三宅さんに教えてもらったものを使ってみました。

Marp というものですが、調べてて気を付けないといけないなと思ったのは最近作り直されたみたいなので、何か調べるときは新しいほうを当たるようにしたほうがよさそうです。 各種機能はこちらから。

marpit.marp.app

ちょっと見た目カスタマイズしたい!

テーマ作ることも出来るみたいなのですが、まだ VSCode の拡張機能のテーマ対応は絶賛作り中みたいです。なので、今回は style で指定する方法でやってみました。

スライド作ってると、表紙・セクションタイトル・普通のスライド・背表紙とか何個かあると思います。私が普段仕事で使ってるスライドは、以下のような見た目です。

f:id:okazuki:20190724175250p:plain

こんな感じのスライドを markdown で書けたら捗りそう!!ということでやってみました。

style

カスタマイズは簡単です。theme を一番癖が無さそうな base にして、 style で CSS 当てていきます。

section と h1 とか footer とかにスタイルを当てるだけでそれっぽくなります。

---
marp: true
theme: base
style: |
    h1, h2, h3, h4, h5, header, footer {
        color: black;
    }
    section {
        background-color: white;
        font-family: 'Yu Gothic UI';
        color: black;
    }
---#表紙!!
![bg 90%](images/bg.png)

2019/xx/xx
Kazuki Ota

---#セクション見出し

---#ほんぶん

-ディオォォオオーーッ
-君が
-泣くまで
-殴るのをやめないッ!

---

![bg 30% vertical](images/logo.png)
<!-- footer: © Copyright Micorsoft Corporation All rights reserved. -->

これで、こんな感じになります。背景画像はパワーポイントのスライドマスターから取り出したい画像を選択した状態で画像として保存で png にして取り出しました。

f:id:okazuki:20190724175829p:plain

さて、あとは一部のページだけ背景を青にしたい。そしてフォントは白にしたいという感じです。そんなときは該当ページのところに

<!-- _class: blue -->

みたいなものを書くと、そのページの section タグの class に blue がつくみたいです。最初のアンダーバーがあるのが、そのページだけという印です。 ということで、スタイルの部分に以下のようなものを付け足します。

section.blue{background-color: #0078D7;
    color: white;
}section.blue>h1,h2,h3,h4,h5,header,footer{color: white;
}

そして、青くしたいページにコメントを差し込みます。markdown ファイルの全体はこんな感じになりました。

---
marp: true
theme: base
style: |
    h1, h2, h3, h4, h5, header, footer {
        color: black;
    }
    section {
        background-color: white;
        font-family: 'Yu Gothic UI';
        color: black;
    }

    section.blue {
        background-color: #0078D7;
        color: white;
    }
    section.blue > h1, h2, h3, h4, h5, header, footer{
        color: white;
    }
---<!-- _class: blue -->#表紙!!
![bg 90%](images/bg.png)

2019/xx/xx
Kazuki Ota

---<!-- _class: blue -->#セクション見出し

---#ほんぶん

-ディオォォオオーーッ
-君が
-泣くまで
-殴るのをやめないッ!

---

![bg 30% vertical](images/logo.png)
<!-- _class: bluefooter: © Copyright Micorsoft Corporation All rights reserved. -->

見た目はこんな感じ。

f:id:okazuki:20190724180340p:plain

許容範囲の見た目かな。

ローカルファイルの画像込みで Export

デフォルトではセキュリティ上の理由でローカルファイルの画像をエクスポート時に見てくれないみたい。VSCode の拡張機能的にはローカルファイルを入れるようにするオプションが見当たらなかった。 なので、以下のコマンドで marp cli をインストールして

npm install -g @marp-team/marp-cli

そして、こんなコマンドで出力します。

# PDF の場合
marp --pdf --allow-local-files test.md
# PPTX の場合
marp --pptx --allow-local-files test.md

試しに PPTX にしてみました。

f:id:okazuki:20190724182709p:plain

完璧!!

WPF の TreeView で任意の項目が表示されるようにスクロールする

$
0
0

というネタを見つけたのでやってみます。久しぶりの WPF ネタ!因みにせっかくなので .NET Core 3.0 Preview 7 で VS2019 Preview 使ってやってみます。

表示用データはこれ!

using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;

namespace TreeViewScrollSample
{
    publicclass Person
    {
        publicstring Name { get; set; }
        public IEnumerable<Person> Children { get; set; }
    }
}

画面が側はこんな感じでいきましょう。

<Windowx:Class="TreeViewScrollSample.MainWindow"xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"xmlns:d="http://schemas.microsoft.com/expression/blend/2008"xmlns:local="clr-namespace:TreeViewScrollSample"xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"Title="MainWindow"Width="800"Height="450"mc:Ignorable="d"><Grid><Grid.RowDefinitions><RowDefinition Height="Auto" /><RowDefinition /></Grid.RowDefinitions><Button Click="ScrollButton_Click"Content="Scroll" /><TreeView x:Name="treeView"Grid.Row="1"><TreeView.ItemTemplate><HierarchicalDataTemplate ItemsSource="{Binding Children}"><TextBlock Text="{Binding Name}" /></HierarchicalDataTemplate></TreeView.ItemTemplate></TreeView></Grid></Window>

これで、こんな感じでコードを書けばスクロールします。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;

namespace TreeViewScrollSample
{
    /// <summary>/// Interaction logic for MainWindow.xaml/// </summary>publicpartialclass MainWindow : Window
    {
        private IEnumerable<Person> People { get; } = Enumerable.Range(1, 100)
            .Select(x => new Person
            {
                Name = $"Tanaka {x}",
                Children = Enumerable.Range(1, 10)
                    .Select(y => new Person
                    {
                        Name = $"Kimura {x}-{y}",
                    })
                    .ToArray(),
            })
            .ToArray();

        public MainWindow()
        {
            InitializeComponent();
            treeView.ItemsSource = People;
        }

        private async void ScrollButton_Click(object sender, RoutedEventArgs e)
        {
            // ContainersGenerated になるまで待つ
            Task waitUntilContainersGenerated(TreeViewItem container)
            {
                var tcs = new TaskCompletionSource<object>(container);
                void statusChanged(object _, EventArgs __)
                {
                    if (container.ItemContainerGenerator.Status == GeneratorStatus.ContainersGenerated)
                    {
                        tcs.SetResult(null);
                        container.ItemContainerGenerator.StatusChanged -= statusChanged;
                        return;
                    }
                }

                container.ItemContainerGenerator.StatusChanged += statusChanged;
                return tcs.Task;
            }

            // 一番最後の最後の要素にスクロールする予定
            var parent = People.Last();
            var target = parent.Children.Last();

            // ツリービュー直下の最後の要素のTreeViewItemを取得
            var container = (TreeViewItem)treeView.ItemContainerGenerator.ContainerFromItem(parent);
            // 開く
            container.IsExpanded = true;
            // 子の ItemContainerGenerator のステータスが Generated になるまで待つif (container.ItemContainerGenerator.Status != GeneratorStatus.ContainersGenerated)
            {
                await waitUntilContainersGenerated(container);
            }

            // スクロール先を取得してスクロール
            var targetContainer = (TreeViewItem)container.ItemContainerGenerator.ContainerFromItem(target);
            targetContainer.BringIntoView();
        }
    }
}

実行すると…

f:id:okazuki:20190725111302g:plain

動いた!!

ソースコードは GitHub に上げておきました。

github.com

ASP.NET WebForm に Vue.js を入れてみよう

$
0
0

Vue.js は Progressive なので出来るでしょう。

プロジェクトの作成

ASP.NET WebForm のプロジェクトを作ります。凄く久しぶりなのですが、普通に ASP.NET Web Application のプロジェクトテンプレートの中の選択肢にありました。

Scripts フォルダーに vue.js をコピーして下準備完了です。

Default.aspx の変更

このページは Vue.js で作る!という意気込みで行くなら簡単です。Scripts フォルダーに default.js を追加して、以下のような感じで Default.aspx を編集します。

<%@ Page Title="Home Page"Language="C#" MasterPageFile="~/Site.Master" AutoEventWireup="true" CodeBehind="Default.aspx.cs" Inherits="VueWebForm._Default" %><asp:ContentID="BodyContent" ContentPlaceHolderID="MainContent" runat="server"><divid="app"><h1>Vue.js app</h1><inputtype="text" v-model="input" /><p>{{ output }}</p></div><scripttype="text/javascript"src="Scripts/vue.js"></script><scripttype="text/javascript"src="Scripts/default.js"></script></asp:Content>

そして default.js に好きなように Vue.js 書いていきます。

const vm = new Vue({
    el: '#app',
    data: function () {return{
            input: ''};
    },
    computed: {
        output: function () {returnthis.input.toUpperCase();
        }}});

動いた。

f:id:okazuki:20190726151158p:plain

注意点

ASP.NET WebForm はページ自身に POST メソッドを投げるというポストバックを通じてページの状態の差分などからイベントを発火したり色々します。 ASP.NET WebForm のコントロールを使ってると、このときに TextBox の入力値とかは保持されるのですが Vue.js の世界からすると単純な再表示できれいさっぱりリセットされます。

なので以下のように WebForm のコントロールのあるページの一部が Vue.js だとすると…

<%@ Page Title="Home Page"Language="C#" MasterPageFile="~/Site.Master" AutoEventWireup="true" CodeBehind="Default.aspx.cs" Inherits="VueWebForm._Default" %><asp:ContentID="BodyContent" ContentPlaceHolderID="MainContent" runat="server"><asp:TextBox runat="server"ID="Input" /><asp:Button runat="server"ID="Button"Text="Click"OnClick="Button_Click" /><divid="app"><h1>Vue.js app</h1><inputtype="text" v-model="input" /><p>{{ output }}</p></div><scripttype="text/javascript"src="Scripts/vue.js"></script><scripttype="text/javascript"src="Scripts/default.js"></script></asp:Content>

実行して適当に入力して…

f:id:okazuki:20190726152057p:plain

ASP.NET WebForms のボタンコントロールのボタンを押すと…

f:id:okazuki:20190726152144p:plain

こんな感じで消えてしまいます。注意すべきなのは TextBox のテキスト変更イベントとかを使ってたら、それでもポストバックが走って Vue.js の画面がリフレッシュされてしまうということです。

ただ、input タグに name を付けてれば、ASP.NET WebForm のポストバックイベントの中で値を参照することは出来ます。 例えば以下のように vueinput という名前を input タグにつけておいて

<%@ Page Title="Home Page"Language="C#" MasterPageFile="~/Site.Master" AutoEventWireup="true" CodeBehind="Default.aspx.cs" Inherits="VueWebForm._Default" %><asp:ContentID="BodyContent" ContentPlaceHolderID="MainContent" runat="server"><asp:TextBox runat="server"ID="Input" /><asp:Button runat="server"ID="Button"Text="Click"OnClick="Button_Click" /><divid="app"><h1>Vue.js app</h1><inputtype="text" v-model="input" /><p>{{ output }}</p><inputname="vueinput"type="hidden" :value="output" /></div><scripttype="text/javascript"src="Scripts/vue.js"></script><scripttype="text/javascript"src="Scripts/default.js"></script></asp:Content>

ボタンのクリックイベントで値をとることができます。

protectedvoid Button_Click(object sender, EventArgs e)
{
    var input = Request.Form["vueinput"];
    Input.Text = $"Hello from {input}";
}

実行して適当に入力して…

f:id:okazuki:20190726153215p:plain

ボタンを押すと Vue.js で大文字変換した結果がちゃんと ASP.NET WebForm のほうの TextBox に出てることが確認できます。

f:id:okazuki:20190726153305p:plain

まとめ

ASP.NET WebForms アプリでも単一ページ全体を Vue.js 化することは割と簡単に出来そう。 でも、ほかのページとステートを共有してとかなんだかんだしてるとめんどくさそうな気がします。

1 ページ内に Vue.js と WebForm が混ざってるとかなりめんどくさそうなので混ぜないのが吉な気がする…。

ということで、WebForms に完全新規ページで、割と独立性の高い機能を追加するのであれば Vue.js を入れることは出来なくはないと思いますが、継続的同じチームで見ていくようなシステムでみんなの合意が取れてる状態じゃないと、メンテナンスで引き取った人がかわいそうな雰囲気を感じました。

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

$
0
0

しました。

Release v6.0.2 · runceel/ReactiveProperty · GitHub

メジャーバージョンが上がってます

ということで、1 つ破壊的変更があります。 これまで WPF では EventToReactiveCommandEventToReactivePropertyを使うのに Blend SDK のアセンブリの Behavior を使用していました。

この Blend SDK は Visual Studio 2019 から同梱されなくなっていて、公式の NuGet パッケージもない状態になりました。そのため、それを置き換える OSS の Behavior のライブラリーである XAML Behaviors for WPF を使用するようにしました。

github.com

そのため、クラス名は同じですが参照元アセンブリや、名前空間が変わっているため、この機能を使用している場合にはコードの変更が必要になります。

更新手順

以下の手順で更新可能です。

  1. ReactiveProperty を v6 以上に更新する
  2. Blend SDK の参照を消す(System.Windows.InteractivityMicrosoft.Expression.Intaractions)
  3. XAML 内の xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"xmlns:i="http://schemas.microsoft.com/xaml/behaviors"に変更する
  4. xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions"がある場合も xmlns:i="http://schemas.microsoft.com/xaml/behaviors"に置き換える

小さな新機能

INotifyPropertyChangedインターフェースを実装したクラスの変更通知機能を持ったクラスのプロパティから ReactivePropertyを生成して同期をとるための ToReactivePropertyAsSynchronizedのソースからターゲット、ターゲットからソースへの変換ロジックを Rx を使って書けるオーバーロードが追加されています。

例えば ReactivePropertySlim<int>ReactiveProperty<string>で値を変換しつつ同期したいケースで、ReactiveProperty<string>が空文字のときは 0 にして、それ以外のケースでは数字としてパース出来るなら ReactivePropertySlim<int>に書き戻したいとします。こんな感じになります。

publicclass MainWindowViewModel
{
    public ReactivePropertySlim<int> Source { get; } = new ReactivePropertySlim<int>();
    public ReactiveProperty<string> Target { get; }

    public MainWindowViewModel()
    {
        Target = Source.ToReactivePropertyAsSynchronized(x => x.Value,
            convert: ox => ox.Select(x => x.ToString()), // int -> string            convertBack: ox => Observable.Merge(
                ox.Where(x => string.IsNullOrEmpty(x)).Select(_ => 0), // 空文字は 0
                ox.Where(x => int.TryParse(x, out _)).Select(x => int.Parse(x)))); // それ以外で数字に変換可能だったら数字にする
    }
}

画面と適当にバインドすると以下のように動きます。Target プロパティを TextBox にバインドして、Source プロパティを TextBlock にバインドしています。

f:id:okazuki:20190726213916g:plain

これまでは ReactivePropertyにバリデーションエラーがある場合のみソースへの変更の反映をスキップするという形しかできませんでしたが、Rx を間に差し込めるようになったので Where などで自由にフィルタリングが出来ます。用途に応じてお使いください。

WPF on .NET Core 3.0 対応

これまでも WPF on .NET Core 3.0 に普通に NuGet から導入可能でしたが、EventToReactivePropertyEventToReactiveCommandは利用できませんでした。 このバージョンから .NET Core 3.0 でも、これらの機能が使えるようにしました。

ReactiveProperty v6 以降と .NET Core 3.0 の WPF のプロジェクトに追加して、Microsoft.Xaml.Behaviors.Wpf パッケージを追加することで使えるようになります。 注意点として、Microsoft.Xaml.Behaviors.Wpf パッケージは、まだ .NET Core 対応のパッケージが出ていないため警告がでます。各自の判断で NU1701 の警告を抑止してお使いください。

Marp で上詰めにする方法

$
0
0

Marp でスライド作るのを色々試してるのですが、全体的にコンテンツを上下方向で中央寄せになってるのが気になりました。 なんとなく上からコンテンツを詰めていくようにしてほしい。ということで sectionタグに以下のようなスタイルを当てることでパワポのように上からコンテンツを配置するようになりました。

section{
    justify-content: start;
}

元のように中央寄せにしたい場合は justify-content: center;にすれば OK です。

Durable Functions の Entity のクラススタイルプログラミングモデルを試してみよう

$
0
0

Durable Functions の Entity のプログラミング体験が割と辛いのですがクラススタイルのプログラミングモデルを使うと既存のプログラミングと同じような感じでいけて素敵です!

クラススタイルのプログラミングモデルは、2019/08/05 時点では Durable Functions v2.0.0-beta1 で試すことが出来ます。

github.com

試してみよう!

早速 Azure Functions のプロジェクトを作ります。そして Microsoft.Azure.WebJobs.Extensions.DurableTaskの 2.0.0-beta1 を追加します。

クラススタイルのエンテティは、普通のクラス!違う点があるとすると、クラス名の FunctionName属性のついた EntityTriggerの関数があるところですね。

using Microsoft.Azure.WebJobs;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
using System;
using System.Threading.Tasks;

namespace FunctionApp1
{
    publicinterface IPerson
    {
        void SetName(string name);
        Task CurseAsync();
    }

    publicclass Person : IPerson
    {
        [JsonProperty("name")]
        publicstring Name { get; set; }
        [JsonProperty("age")]
        publicint Age { get; set; }

        publicvoid SetName(string name) => Name = name;
        // 適当な時間経過後に加齢する呪いpublic async Task CurseAsync()
        {
            var r = new Random();
            await Task.Delay(TimeSpan.FromSeconds(r.Next(10)));
            Age += r.Next(10);
        }

        [FunctionName(nameof(Person))]
        publicstatic Task Run([EntityTrigger] IDurableEntityContext ctx)
            => ctx.DispatchAsync<Person>();
    }
}

この EntityTriggerのついた関数の DispatchAsyncが凄くいい仕事します。Person クラスの関数をいい感じに呼び出してくれます。

HttpTrigger などから呼び出してみよう

HttpTrigger などから Entity を使うには、IDurableOrchestrationClient#SignalEntityAsyncでメソッドを呼び出して、IDurableOrchestrationClient#ReadEntityStateAsyncで Entity のステータスを取得します。ステータスは、先ほど定義した Person クラスのプロパティに値が入ってる感じのものが返ってきます。

例えば…適当に SetName を呼んでステータスを読み取ってみましょう。

[FunctionName("CreatePerson")]
publicstatic async Task<IActionResult> CreatePersonAsync(
    [HttpTrigger(AuthorizationLevel.Function, "get", "post")]HttpRequest req,
    [OrchestrationClient] IDurableOrchestrationClient client)
{
    var id = CreateEntityIdFrom(req);
    if (!id.HasValue)
    {
        returnnew BadRequestResult();
    }

    await client.SignalEntityAsync<IPerson>(id.Value, proxy => proxy.SetName(id.Value.EntityKey));
    returnnew OkObjectResult(await client.ReadEntityStateAsync<Person>(id.Value));
}

privatestatic EntityId? CreateEntityIdFrom(HttpRequest req)
{
    var name = req.Query["name"];
    if (string.IsNullOrEmpty(name))
    {
        returnnull;
    }

    returnnew EntityId(nameof(Person), name);
}

EntityIdの最初の引数が Person クラスに定義した関数の名前です。そして、EntityIdの第二引数に Entity の識別子みたいな感じですね。今回は HttpTrigger の URL のクエリーパラメータの name でを使う感じにしました。

POST http://localhost:7071/api/CreatePerson?name=kazuakix HTTP/1.1こんな感じのURL を叩くと下のような結果が返ってきます。

HTTP/1.1 200 OK
Connection: close
Date: Mon, 05 Aug 2019 11:30:47 GMT
Content-Type: application/json; charset=utf-8
Server: Kestrel
Content-Length: 41

{
  "entityExists": false,
  "entityState": null
}

entityStateが null なのが気になりますが、これは SignalEntityAsyncは処理の完了を待たずに、処理の要求がキューに入ったら戻ってくる感じです。ReadEntityStateAsyncも別に SignalEntityAsyncで何か処理が行われるか気にせず結果を返すので、こんな結果になります。

なので、もう一度同じ HttpTrigger の関数を叩くと以下のような結果になります。

HTTP/1.1 200 OK
Connection: close
Date: Mon, 05 Aug 2019 11:34:36 GMT
Content-Type: application/json; charset=utf-8
Server: Kestrel
Content-Length: 63

{
  "entityExists": true,
  "entityState": {
    "name": "kazuakix",
    "age": 0
  }
}

他に、Person#CurseAsyncを呼ぶ場合は以下のようなコードになります。

[FunctionName("Curse")]
publicstatic async Task<IActionResult> CurseAsync(
    [HttpTrigger(AuthorizationLevel.Function, "get", "post")]HttpRequest req,
    [OrchestrationClient] IDurableOrchestrationClient client)
{
    var id = CreateEntityIdFrom(req);
    if (!id.HasValue)
    {
        returnnew BadRequestResult();
    }

    await client.SignalEntityAsync<IPerson>(id.Value, proxy => proxy.CurseAsync());
    returnnew OkObjectResult(await client.ReadEntityStateAsync<Person>(id.Value));
}

この例でも CurseAsyncを処理するという要求をするだけなので、最後の結果の ReadEntityStateAsyncの結果は加齢前のものになります。試しに呼んでみると…

HTTP/1.1 200 OK
Connection: close
Date: Mon, 05 Aug 2019 11:36:45 GMT
Content-Type: application/json; charset=utf-8
Server: Kestrel
Content-Length: 63

{
  "entityExists": true,
  "entityState": {
    "name": "kazuakix",
    "age": 0
  }
}

年齢は変わりません。ちょっと何もせずステータスを返す以下のような関数を作って…

[FunctionName("GetPerson")]
publicstatic async Task<IActionResult> GetPersonAsync(
    [HttpTrigger(AuthorizationLevel.Function, "get", "post")]HttpRequest req,
    [OrchestrationClient] IDurableOrchestrationClient client)
{
    var id = CreateEntityIdFrom(req);
    if (!id.HasValue)
    {
        returnnew BadRequestResult();
    }

    returnnew OkObjectResult(await client.ReadEntityStateAsync<Person>(id.Value));
}

適当な時間経過後に呼んでみましょう。

HTTP/1.1 200 OK
Connection: close
Date: Mon, 05 Aug 2019 11:37:46 GMT
Content-Type: application/json; charset=utf-8
Server: Kestrel
Content-Length: 63

{
  "entityExists": true,
  "entityState": {
    "name": "kazuakix",
    "age": 4
  }
}

年齢増えてますね。

処理結果を待ちつつ結果を取りたい

そういうときは OrchestrationTriggerの中で Entity を使うと良さそう。

[FunctionName("CurseAndWait")]
publicstatic async Task<IActionResult> CurseAndWaitAsync(
    [HttpTrigger(AuthorizationLevel.Function, "get", "post")]HttpRequest req,
    [OrchestrationClient] IDurableOrchestrationClient client)
{
    var id = CreateEntityIdFrom(req);
    if (!id.HasValue)
    {
        returnnew BadRequestResult();
    }

    var instanceId = await client.StartNewAsync("CurseAndWaitOrchestrator", id.Value);
    while((await client.GetStatusAsync(instanceId)).RuntimeStatus != OrchestrationRuntimeStatus.Completed)
    {
        await Task.Delay(5000);
    }

    returnnew OkObjectResult(await client.ReadEntityStateAsync<Person>(id.Value));
}

[FunctionName("CurseAndWaitOrchestrator")]
publicstatic async Task CurseAndWaitOrchestratorAsync(
    [OrchestrationTrigger]IDurableOrchestrationContext context)
{
    var id = context.GetInput<EntityId>();

    var proxy = context.CreateEntityProxy<IPerson>(id);
    await proxy.CurseAsync();
}

オーケストレーター関数の終了待つのに、WaitForCompletionOrCreateCheckStatusResponseAsync使えばいいやって思ったけど、この関数は、あくまでレスポンス受け取った先の人のためのメソッドであって、HttpTrigger の中でこの関数を await しても必ずしも完了まで待ってくれないんですね。勉強になった。 ということでステータスが完了になるまで自分で待ってます。

そうすると、関数を呼ぶと加齢が完了して、結果に加齢後のオブジェクトが入ってます。 呼び出し前の状態を確認するために GetPerson 関数を呼んで結果を見てます。

HTTP/1.1 200 OK
Connection: close
Date: Mon, 05 Aug 2019 12:02:23 GMT
Content-Type: application/json; charset=utf-8
Server: Kestrel
Content-Length: 64

{
  "entityExists": true,
  "entityState": {
    "name": "kazuakix",
    "age": 47
  }
}

47 歳ですね。では先ほど作った CurseAndWait関数をこんな感じ( GET http://localhost:7071/api/CurseAndWait?name=kazuakix HTTP/1.1 )で呼んでみます。

HTTP/1.1 200 OK
Connection: close
Date: Mon, 05 Aug 2019 12:02:50 GMT
Content-Type: application/json; charset=utf-8
Server: Kestrel
Content-Length: 64

{
  "entityExists": true,
  "entityState": {
    "name": "kazuakix",
    "age": 53
  }
}

53 歳になりましたね。あってるのかな。

まとめ

Durable Functions の Entity が本当に Entity 書いてるみたいになるのでとてもいい。Durable Functions v2 のリリースが楽しみになりますね。

Azure App Service の Easy Tables と Easy API が消える…!?

$
0
0

azure.github.io

2019/11/09 に消えちゃうみたいです。

GUI が無くなるだけで、まぁ結局は node.js のコードとか設定ファイル書けば動くよ~ということではあるのですが、Mobile Apps 関連機能はメンテナンスモードにもなってるので、Visual Studio App Center の方への移行を検討するか、自分たちで作るかということになるのかなぁという感じです。


Azure Boards(タスク管理ツール)が Slack 対応しました

$
0
0

docs.microsoft.com

へ~、まじで。試してみよう! ということで Slack でアプリ追加をしてみます。アプリ一覧で Azure で検索するとありました。。

f:id:okazuki:20190806093710p:plain

Pipelines (自動ビルド)もあるんですね。まぁ今回は Azure Boards をインストールします。 インストール時の設定項目はこんな感じ。

f:id:okazuki:20190806093921p:plain

インストールするとチャンネルに以下のようなメッセージが出てきます。

f:id:okazuki:20190806094008p:plain

では、/azboards signinをしてみます。 すると、以下のようなメッセージが出てきます。

f:id:okazuki:20190806094051p:plain

Sign inボタンを押すとサインインして以下のような承認画面が出てきます。

f:id:okazuki:20190806094227p:plain

承認をすると数字が表示されるので、それを Slack の Enter Codeとなっている場所に打ち込みます。 そうすると以下のように言われます。

f:id:okazuki:20190806094336p:plain

言われた通り紐づけしたい Azure DevOps のプロジェクトの URL をコマンドで設定します。

f:id:okazuki:20190806094532p:plain

Add subscription...ボタンを押して適当に購読する変更を設定します。

f:id:okazuki:20190806094811p:plain

では、/azboards createをしてみます。

f:id:okazuki:20190806094904p:plain

作るワークアイテムのタイプを選んで、どのエリアか選択するとタイトルと詳細を書く画面が出てきました。

f:id:okazuki:20190806095004p:plain

作り終わると、ワークアイテムが作られた時にメッセージを出すように設定してたのでちゃんとメッセージが出てきました。

f:id:okazuki:20190806095057p:plain

メッセージ内のリンクをクリックすると Azure Boards の該当のワークアイテムがブラウザーで開くので適当に自分をアサインしてステータスを Doing に変えてみました。そうすると、Slack のほうにちゃんと結果が出ました!

f:id:okazuki:20190806095312p:plain

まとめ

これいいね。

Azure Kubernetes Service のハンズオンしてきた!その復習

$
0
0

お盆で帰省してたタイミングで丁度下のイベントが行われていたので参加者として参加してきました!

hiroshima-jug.connpass.com

今まで Azure だと Web App とかで割となんとかなっていたので使うことはなかったのですが興味はあったので丁度いいと思ったのがきっかけ。

復習もかねて実際に使った以下のリポジトリーの内容を見ながら自分でもやってメモっておこうと思います。

github.com

Azure Kubernetes Service (AKS)

名前のとおり Azure の Kubernetes のサービス。 ノード数とノードのスペックくらいを設定しておけば、あとは割とよしなにやってくれるみたい。急激にスパイクしたときは Azure Container Instance のほうに展開するとかいうこともできるみたい。ノード立ち上げたりすると時間がかかるしね。

作ってみよう

さくっと Kubernetes Service を作ってみた

f:id:okazuki:20190812090648p:plain

f:id:okazuki:20190812090710p:plain

モブプログラミング形式でみんなでやったハンズオンでは、確か下の HTTP の機能を有効にしたけど…

f:id:okazuki:20190812091048p:plain

本番では、HTTPS の構成をしたほうがよさそうということがドキュメントに書いてあった。

こんな感じで。

docs.microsoft.com

とりあえず ON にして始めてみよう。

Azure Container Registry の作成

プライベートな DockerHub みたいな ACR も作っておきます。 これは特筆事項もとくにないくらい作るだけです。

作業用マシンを作ろう

私は Azure 上に Linux VM を一台立ててるのでそれを使います。 az コマンドのインストールと

docs.microsoft.com

docker を入れておきます。

docs.docker.com

ACR へのログインを docker コマンドでしておきます。ACR のアクセスキーのところで見れます。管理者ユーザーっていうのを有効にするみたい。

docker login -u ユーザー名 -p "パスワード" ACRの名前.azurecr.io

実際に値を当てはめるとこんなかんじ。

sudo docker login -u kazukicr -p "sugo-kunagai-portal-kara-toreru-pasuwa-do-dayo!" kazukicr.azurecr.io

適当に ACR に push

ということでついに docker push の機運が高まってきました。Azure Functions でやってみます。

以下のように --dockerオプションをつけておくと docker ファイルもできます。便利。

func init --docker

そして、何もないと寂しいので func newで HttpTrigger の関数でも追加しておきます。そして以下のコマンドをたたいてビルド

sudo docker build .

ビルドが通ったので、とりあえずタグ付けとか docker pushするスクリプトも書いておきます。これも寺田さん作のもの

#!/bin/bashset-eif ["$1"=""]thenecho"./build-create.sh [version-number]"exit1fiexport VERSION=$1DOCKER_IMAGE=okazukirc/dockerfunc
DOCKER_REPOSITORY=kazukicr.azurecr.io

docker build -t$DOCKER_IMAGE:$VERSION . -f ./Dockerfile
docker tag $DOCKER_IMAGE:$VERSION$DOCKER_REPOSITORY/$DOCKER_IMAGE:$VERSION
docker push $DOCKER_REPOSITORY/$DOCKER_IMAGE:$VERSION

docker rm$(docker ps -aq)
docker images | awk '/<none/{print $3}' | xargs docker rmi

bash のスクリプト慣れないなぁ。chmod して sudo ./docker-build.sh 1.0してビルド成功!!

ACR の画面を見るとちゃんと push されてました。めでたい。

f:id:okazuki:20190815131906p:plain

ちょっとローカルで動かしてみましょう。以下のコマンドをたたいて…

$ sudo docker run -p 8080:80 kazukicr.azurecr.io/okazukirc/dockerfunc:1.0

curl でたたくと…

$ curl http://local/Hello?name=aaaaaa
Hello, aaaaaa

動きましたね。

ついに Kubernetes

kubectlを入れます。コマンドでさくっと

$ az aks install-cli

コマンドが入ったので AKS への認証情報をゲットしましょう。

$ az aks get-credentials --resource-group AKS-rg --name kazukiaks
Merged "kazukiaks" as current context in /home/okazuki/.kube/config

ノードも取れました。

$ kubectl get node
NAME                       STATUS   ROLES   AGE    VERSION
aks-agentpool-51334753-0   Ready    agent   3d4h   v1.12.8
aks-agentpool-51334753-1   Ready    agent   3d4h   v1.12.8
aks-agentpool-51334753-2   Ready    agent   3d4h   v1.12.8
virtual-node-aci-linux     Ready    agent   3d4h   v1.13.1-vk-v0.9.0-1-g7b92d1ee-dev

AKSからACRへのアクセスの認証情報を追加します。

$ kubectl create secret docker-registry \
>   docker-reg-credential \
>   --docker-server=kazukicr.azurecr.io \
>   --docker-username=kazukicr \
>   --docker-password=your-password \
>   --docker-email=k_ota28@hotmail.com

ここら辺から、デプロイメントやサービスなどの定義の YAML を適用するのですが、ハンズオン時には出来上がった YAML の一部を編集して使ってたのでよくわからんって感じになりますね。

まず、Deployment については下を見ました。

kubernetes.io

今回の場合はこんな感じだろうか??

apiVersion: apps/v1
kind: Deployment
metadata:name: dockerfunc-deployment
  labels:app: dockerfunc
spec:replicas:3selector:matchLabels:app: dockerfunc
  template:metadata:labels:app: dockerfunc
    spec:securityContext:runAsUser:1000imagePullSecrets:- name: docker-reg-credential
      containers:- name: dockerfunc
        image: kazukicr.azurecr.io/okazukirc/dockerfunc:1.0
        ports:- containerPort:80

imagePullSecretsが先ほど kubectlで作った secret の名前です。runAsUserは、ルートで動かさないという感じ。1000 にしてるってことは最初に作られたユーザーアカウントってことなんだろう。

kubectl apply -f ファイル名で適用できるので、上記ファイルを deploy.yamlという名前で保存している場合は以下のようなコマンドで適用できます。

$ kubectl apply -f deploy.yaml 
deployment.apps/dockerfunc-deployment created

deployment がちゃんとできてるかコマンドを打って確認します。

$ kubectl get deploy
NAME                    DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
dockerfunc-deployment   3         3         3            0           74s

出来てるっぽい。Pod は…?

$ kubectl get po
NAME                                    READY   STATUS             RESTARTS   AGE
dockerfunc-deployment-76bb667bd-6cl68   0/1     CrashLoopBackOff   4          2m43s
dockerfunc-deployment-76bb667bd-9jnsh   0/1     CrashLoopBackOff   4          2m43s
dockerfunc-deployment-76bb667bd-vtcg8   0/1     CrashLoopBackOff   4          2m43s

CrashLoopBackOff なので激しく失敗してますね…。エラーのときは kubectl descriptionコマンドでログ見てみるといいといわれたので見てみます。

$ kubectl describe pods dockerfunc-deployment-76bb667bd-6cl68

結果はこれ…よくわからない...

Name:           dockerfunc-deployment-68984c94bd-4vbr2
Namespace:      default
Priority:       0
Node:           aks-agentpool-51334753-1/10.240.0.66
Start Time:     Thu, 15 Aug 2019 05:18:22 +0000
Labels:         app=dockerfunc
                pod-template-hash=68984c94bd
Annotations:    <none>
Status:         Running
IP:             10.240.0.68
Controlled By:  ReplicaSet/dockerfunc-deployment-68984c94bd
Containers:
  dockerfunc:
    Container ID:   docker://c62fc1f1d2a3818651d0b703140b3119ad3755ffb8cb99efc49ea563c15c2ddc
    Image:          kazukicr.azurecr.io/okazukirc/dockerfunc:1.0
    Image ID:       docker-pullable://kazukicr.azurecr.io/okazukirc/dockerfunc@sha256:2ef5515278a6f17053dfc14289fdf5f77ea7d127819d75f6951b1beac00a212e
    Port:           80/TCP
    Host Port:      0/TCP
    State:          Terminated
      Reason:       Error
      Exit Code:    1
      Started:      Thu, 15 Aug 2019 05:19:05 +0000
      Finished:     Thu, 15 Aug 2019 05:19:05 +0000
    Last State:     Terminated
      Reason:       Error
      Exit Code:    1
      Started:      Thu, 15 Aug 2019 05:18:39 +0000
      Finished:     Thu, 15 Aug 2019 05:18:39 +0000
    Ready:          False
    Restart Count:  3
    Limits:
      cpu:  100m
    Requests:
      cpu:        100m
    Environment:  <none>
    Mounts:
      /var/run/secrets/kubernetes.io/serviceaccount from default-token-psnwx (ro)
Conditions:
  Type              Status
  Initialized       True 
  Ready             False 
  ContainersReady   False 
  PodScheduled      True 
Volumes:
  default-token-psnwx:
    Type:        Secret (a volume populated by a Secret)
    SecretName:  default-token-psnwx
    Optional:    false
QoS Class:       Burstable
Node-Selectors:  <none>
Tolerations:     node.kubernetes.io/not-ready:NoExecute for 300s
                 node.kubernetes.io/unreachable:NoExecute for 300s
Events:
  Type     Reason     Age               From                               Message
  ----     ------     ----              ----                               -------
  Normal   Scheduled  50s               default-scheduler                  Successfully assigned default/dockerfunc-deployment-68984c94bd-4vbr2 to aks-agentpool-51334753-1
  Normal   Pulled     8s (x4 over 49s)  kubelet, aks-agentpool-51334753-1  Container image "kazukicr.azurecr.io/okazukirc/dockerfunc:1.0" already present on machine
  Normal   Created    8s (x4 over 49s)  kubelet, aks-agentpool-51334753-1  Created container
  Normal   Started    7s (x4 over 48s)  kubelet, aks-agentpool-51334753-1  Started container
  Warning  BackOff    7s (x5 over 47s)  kubelet, aks-agentpool-51334753-1  Back-off restarting failed container

とりあえず RunAsUser を外してみたら動いた…う~ん。

apiVersion: apps/v1
kind: Deployment
metadata:name: dockerfunc-deployment
  labels:app: dockerfunc
    version: v1
spec:replicas:3selector:matchLabels:app: dockerfunc
  template:metadata:labels:app: dockerfunc
    spec:#      securityContext:#        runAsUser: 1000imagePullSecrets:- name: docker-reg-credential
      containers:- name: dockerfunc
        image: kazukicr.azurecr.io/okazukirc/dockerfunc:1.0
        ports:- containerPort:80resources:requests:cpu: 100m
          limits:cpu: 100m

apply したら動いた

$ kubectl get po
NAME                                     READY   STATUS    RESTARTS   AGE
dockerfunc-deployment-674d45656d-jgb9b   1/1     Running   0          119s
dockerfunc-deployment-674d45656d-lsbf4   1/1     Running   0          115s
dockerfunc-deployment-674d45656d-mnb8v   1/1     Running   0          2m1s

ポートフォワーディングして動いてるか確認しましょう。

$ kubectl port-forward dockerfunc-deployment-674d45656d-jgb9b 8080:80
Forwarding from 127.0.0.1:8080 -> 80
Forwarding from [::1]:8080 -> 80

curl でたたいてみると…

$ curl http://localhost:8080/api/Hello?name=okazuki
Hello, okazuki

動いた!!

サービス

pod をまとめるサービスかな。それの定義を作りましょう。

apiVersion: v1
kind: Service
metadata:labels:app: dockerfunc-service
  name: dockerfunc-service
spec:ports:- port:80name: http
    targetPort:80selector:app: dockerfunc
  sessionAffinity: None
  type: LoadBalancer

dockerfunc の v1 で選択したやつをさくっとまとめておきます。最後の type: LoadBalancerは本番では使わないで!って言われたけど、とりあえず動きを確認するために設定。これをしてると簡単に外から叩ける。

適用します。

$ kubectl apply -f service.yaml 
service/dockerfunct-service created

しばらく待つと IP が割り当てられます。(今回は 13.71.149.89)

$ kubectl get service
NAME                  TYPE           CLUSTER-IP   EXTERNAL-IP    PORT(S)        AGE
dockerfunc-service   LoadBalancer   10.0.52.83   13.71.149.86   80:31505/TCP   7m4s
kubernetes            ClusterIP      10.0.0.1     <none>         443/TCP        3d5h

curl でたたくと…

$ curl http://13.71.149.86/api/Hello?name=kubernates
Hello, kubernates

動いた!!

では、サービスを LoadBalancer で公開するのをやめます。ClusterIP にしておくのがよさそう。ClusterIP にしたら一度サービス削除して再度アプライしました。

$ kubectl delete service dockerfunc-service
$ kubectl apply -f service.yaml

Ingress を作成

Ingress がサービスに処理を割り振ってくれるような動きをしてくれるものみたいです。これを作って、ここにサービスの割り振りを設定することでちゃんと外からアクセスできるようになります。

Ingress を作りにあたってドメインをゲットしましょう。ポータルの AKS を開くと HTTP アプリケーションのルーティング ドメインという項目があります。これを控えておきます。

f:id:okazuki:20190815150534p:plain

そして、dockerfunc-service に対してアクセスできるようにしましょう。Ingress を定義する YAML を定義して…

apiVersion: extensions/v1beta1
kind: Ingress
metadata:name: dockerfunc
  annotations:kubernetes.io/ingress.class: addon-http-application-routing
spec:rules:- host: dockerfunc.ada93e44ae4b4d9ea00a.japaneast.aksapp.io
    http:paths:- backend:serviceName: dockerfunc-service 
          servicePort:80path: /

apply しましょう。

$ kubectl apply -f ingress.yaml
ingress.extensions/dockerfunc created

kubectl get ingで Ingress の状態が見れます。

$ kubectl get ing
NAME         HOSTS                                                 ADDRESS          PORTS   AGE
dockerfunc   dockerfunc.ada93e44ae4b4d9ea00a.japaneast.aksapp.io   104.41.172.168   80      52s

では、curl でアクセスしてみましょう。

$ curl http://dockerfunc.ada93e44ae4b4d9ea00a.japaneast.aksapp.io/api/Hello?name=Ingress
Hello, Ingress

動いた!!

バージョンアップしてみよう

ちょっと Azure Functions のコードを変えていきます。

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;

namespace DockerFunc
{
    publicstaticclass Hello
    {
        [FunctionName("Hello")]
        publicstatic async Task<IActionResult> Run(
            [HttpTrigger(AuthorizationLevel.Anonymous, "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;

            return name != null
                ? (ActionResult)new OkObjectResult($"こんにちは, {name}")
                : new BadRequestObjectResult("Please pass a name on the query string or in the request body");
        }
    }
}

ビルドスクリプトを実行して version 2.0 を作ります。

$ sudo ./docker-build.sh 2.0

できました。

f:id:okazuki:20190815151254p:plain

deployment を新しくして…

apiVersion: apps/v1
kind: Deployment
metadata:
  name: dockerfunc-deployment
  labels:
    app: dockerfunc
    version: v1
spec:
  replicas: 3
  selector:
    matchLabels:
      app: dockerfunc
  template:
    metadata:
      labels:
        app: dockerfunc
        version: v2
    spec:
#      securityContext:
#        runAsUser: 1000
      imagePullSecrets:
        - name: docker-reg-credential
      containers:
      - name: dockerfunc
        image: kazukicr.azurecr.io/okazukirc/dockerfunc:2.0
        ports:
        - containerPort: 80
        resources:
          requests:
            cpu: 100m
          limits:
            cpu: 100m

apply してみましょう。

$ kubectl apply -f deploy.yaml 
deployment.apps/dockerfunc-deployment configured

curl でたたいてみると…

$ curl http://dockerfunc.ada93e44ae4b4d9ea00a.japaneast.aksapp.io/api/Hello?name=Ingress
こんにちは, Ingress

ふむ…

deployment のラベルを完全に別名にして、サービスを新たに作って Ingress で新しいサービスを指し示すようにしたりすると古いサービスに戻すとかもできそう。

まとめ

まだまだ、いろいろな機能があるんだろうね…。 でも、とっかかりにとてもいいイベントでした!!

ちなみに実際には、このほかにも Azure DevOps を使って AKS に自動ビルドして自動デプロイするようなものもやりました。 一日なのに盛沢山。

ASP.NET Core 3.0 で gRPC してみよう

$
0
0

.NET Core になると WCF のサーバーサイドが消えて移行先として gRPC があげられてるのを何処かで見た気がします。OSS の WCF もあった気がするけど、そっちはよく見てない。

ということで、ASP.NET Core 3.0 Preview で gRPC 試してみようと思います。

プロジェクトの作成

今日は出先のカフェでコーヒー飲みながら Surface Go で書いてます。なので Visual Studio 2019 は入ってない(Surface Go には重すぎた)ので、Visual Studio Code でいきます。

適当なフォルダーで空の Web アプリを作ります。

$ dotnet new web -o GrpcServer

ソリューションも作って追加しておきましょう。

$ dotnet new sln
$ dotnet sln add GrpcServer/GrpcServer.csproj

Visual Studio Code でソリューションのあるフォルダーを開いて task.json とかを生成して Ctrl + Shift + Bでビルドしたり F5でデバッグできるようにしました。便利。

f:id:okazuki:20190830143509p:plain

gRPC のためのパッケージを追加します。

$ dotnet add .\GrpcServer\GrpcServer.csproj package Grpc.AspNetCore -v 0.1.22-pre3

そして、.protoファイルを生成しましょう。これも dotnet newで生成できます。後で作成するクライアントでも使う予定なので、ソリューションのあるフォルダーに Proto/Proto.protoとかみたいな感じで作りました。

$ dotnet new proto -o Proto

サービスの定義を追加します。

syntax = "proto3";

option csharp_namespace = "GrpcSample";

service Greeter {
    rpc Greet (GreetRequest) returns (GreetReply);
}

message GreetRequest {
    string name = 1;
}

message GreetReply {
    string message = 1;
}

サーバープロジェクトに追加するために .csprojを編集しましょう。

<Project Sdk="Microsoft.NET.Sdk.Web"><PropertyGroup><TargetFramework>netcoreapp3.0</TargetFramework></PropertyGroup><ItemGroup><PackageReference Include="Grpc.AspNetCore"Version="0.1.22-pre3" /></ItemGroup><ItemGroup><!-- これを追加 --><Protobuf Include="../Proto/Proto.proto"LinkBase="Proto/Proto.proto"GrpcServices="Server" /></ItemGroup></Project>

これでビルドをすると GrpcServer/obj/DebugProto.csProtoGrpc.csが生成されます。これを継承してサービスを実装します。

GrpcServerプロジェクトに Servicesフォルダーを作って、そこに GreeterService.csを作って以下のように Greeter.GreeterBaseを継承する形で処理を作ります。

using System.Threading.Tasks;
using Grpc.Core;
using GrpcSample;

namespace GrpcService.Services
{
    publicclass GreeterService : Greeter.GreeterBase
    {
        publicoverride Task<GreetReply> Greet(GreetRequest request, ServerCallContext context)
        {
            return Task.FromResult(new GreetReply
            {
                Message = $"Hello {request.Name}",
            });
        }
    }
}

.protoに定義したクラスとサービスのメソッドのひな型は基本クラスで定義されているので、やることはメソッドをオーバーライドして実装するだけです。簡単。

Startup.csで gRPC 機能の有効化と上で作成したサービスを登録する処理を追加します。

using GrpcService.Services;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace GrpcServer
{
    publicclass Startup
    {
        publicvoid ConfigureServices(IServiceCollection services)
        {
            services.AddGrpc(); // これと
        }

        publicvoid Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseRouting();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapGrpcService<GreeterService>(); // これを追加
                endpoints.MapGet("/", async context =>
                {
                    await context.Response.WriteAsync("Hello World!");
                });
            });
        }
    }
}

これでサーバーは完成しましたといいたいところですが、もうちょっとだけ設定を… gRPC は HTTP/2 を使うので、その設定を追加します。appsettings.jsonに以下のような設定を追加します。

{"Logging": {"LogLevel": {"Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }},
  "AllowedHosts": "*",
  "Kestrel": {"EndpointDefaults": {"Protocols": "Http2"
    }}}

Kestrel の部分が追加したものになります。

クライアントの作成

サーバーだけ作っても、誰も呼んでくれないと何もできないのでクライアントも作ります。WPF でいきましょう。さくっとプロジェクトを作ってソリューションに追加します。

$ dotnet new wpf -o GrpcClient
$ dotnet sln add .\GrpcClient\GrpcClient.csproj

そして gRPC のクライアント側に必要なパッケージを入れます。

dotnet add .\GrpcClient\GrpcClient.csproj package Google.Protobuf -v 3.9.1
dotnet add .\GrpcClient\GrpcClient.csproj package Grpc.Net.Client -v 0.1.22-pre3
dotnet add .\GrpcClient\GrpcClient.csproj package Grpc.Tools -v 2.23.0 

そして、GrpcClient.csprojに Protobuf タグを追加します。今回は生成してもらうのはクライアントなので GrpcServices 属性には Client を設定してます。余談ですがクライアントとサーバーの両方を生成してほしいときは Both とかくみたいです。

<Project Sdk="Microsoft.NET.Sdk.WindowsDesktop"><PropertyGroup><OutputType>WinExe</OutputType><TargetFramework>netcoreapp3.0</TargetFramework><UseWPF>true</UseWPF></PropertyGroup><ItemGroup><PackageReference Include="Google.Protobuf"Version="3.9.1" /><PackageReference Include="Grpc.Net.Client"Version="0.1.22-pre3" /><PackageReference Include="Grpc.Tools"Version="2.23.0"><IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets><PrivateAssets>all</PrivateAssets></PackageReference></ItemGroup><ItemGroup><Protobuf Include="../Proto/Proto.proto"LinkBase="Proto/Proto.proto"GrpcServices="Client" /></ItemGroup></Project>

そして、名前を入力するための TextBox とサービスを呼ぶための Button を置いた画面を MainWindow.xamlに定義して…

<Window x:Class="GrpcClient.MainWindow"xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"xmlns:d="http://schemas.microsoft.com/expression/blend/2008"xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"xmlns:local="clr-namespace:GrpcClient"mc:Ignorable="d"Title="MainWindow"Height="450"Width="800"><StackPanel><TextBox x:Name="textBoxName" /><Button Content="Call gRPC service"Click="CallGrpcServiceButton_Click" /></StackPanel></Window>

コードビハインドにサービスを呼び出すコードを書きましょう。

using System;
using System.Net.Http;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using GrpcSample;

namespace GrpcClient
{
    /// <summary>/// Interaction logic for MainWindow.xaml/// </summary>publicpartialclass MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        private async void CallGrpcServiceButton_Click(object sender, RoutedEventArgs e)
        {
            using (var client = new HttpClient
            {
                BaseAddress = new Uri("https://localhost:5001")
            })
            {
                var greetServices = Grpc.Net.Client.GrpcClient.Create<Greeter.GreeterClient>(client);
                var response = await greetServices.GreetAsync(new GreetRequest
                {
                    Name = textBoxName.Text,
                });
                MessageBox.Show(response.Message);
            }
        }
    }
}

そうするとコンパイルエラー!!

MainWindow.xaml.cs(16,7): error CS0246: The type or namespace name 'GrpcSample' could not be found (are you missing a using directive or an assembly reference?) [c:\Users\k_ota\source\repos\GrpcLab\GrpcClient\GrpcClient_teh220ey_wpftmp.csproj]

起きてるエラーとしては以下の Issue と似てるけど、こっちはクラシックツールチェーン…

github.com

試しに WPF じゃなくてコンソールアプリで同じ手順を踏んで呼び出す処理を書いたらコンパイルエラーにならないので WPF on .NET Core 用のツールまわりのバグかな?とりあえずの回避方法はクライアントコードの生成をクラスライブラリにうつすことです。

プロジェクトを作成して、必要な参照を追加したりします。

$ dotnet new classlib -o GrpcClientLib
$ dotnet sln add .\GrpcClientLib\GrpcClientLib.csproj

生成されるのは .NET Standard 2.0 のプロジェクトなのですが、Grpc.Net.Client 0.1.22-pre3 は .NET Standard 2.1 (ターゲットにしてるライブラリ始めてみた!)なので GrpcClientLib.csproj の netstandard2.0 を netstandard2.1 に書き換えてから下記コマンドでライブラリを追加します。

$ dotnet add .\GrpcClientLib\GrpcClientLib.csproj package Google.Protobuf -v 3.9.1
$ dotnet add .\GrpcClientLib\GrpcClientLib.csproj package Grpc.Net.Client -v 0.1.22-pre3
$ dotnet add .\GrpcClientLib\GrpcClientLib.csproj package Grpc.Tools -v 2.23.0 

そして GrpcClientLib.csprojに、Protobuf タグの定義を追加します。

<Project Sdk="Microsoft.NET.Sdk"><PropertyGroup><TargetFramework>netstandard2.0</TargetFramework></PropertyGroup><ItemGroup><PackageReference Include="Google.Protobuf"Version="3.9.1" /><PackageReference Include="Grpc.Tools"Version="2.23.0"><IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets><PrivateAssets>all</PrivateAssets></PackageReference></ItemGroup><ItemGroup><Protobuf Include="../Proto/Proto.proto"LinkBase="Proto/Proto.proto"GrpcServices="Client" /></ItemGroup></Project>

WPF 側からは Protobuf のタグを削除しておきましょう。

そして、GrpcClient に GrpcCLientLib への参照を追加します。

$ dotnet add .\GrpcClient\GrpcClient.csproj reference .\GrpcClientLib\GrpcClientLib.csproj

これでコンパイルエラーなしでビルドが通るようになります。

まだ一度も入れたことがない人は .NET Core の開発用の証明書をインストールして

$ dotnet dev-certs https --trust

dotnet runでサーバーとクライアントを起動して試してみましょう。

まずは、サーバー

$ dotnet run --project .\GrpcServer\GrpcServer.csproj

そして、クライアント

$ dotnet run --project .\GrpcClient\GrpcClient.csproj

適当に TextBox に何か入れてボタンを押すと無事動きました

f:id:okazuki:20190830160819p:plain

Azure にデプロイ!!

Azure の App Service に gRPC のサービスをデプロイして動かすことはできないみたいです。残念。

github.com

AKS 使えば出来そうですが、ここに書くにはちょっとヘビーなので、また今度トライしてみて書きます。

まとめ

ASP.NET Core の gRPC 割とサクッと作れていい感じです。 LTS 版の .NET Core 3.1 が出たら使ってみたいなぁ。でも App Service の対応は早くしてほしいところ。

ソースコードは、GitHub にあげておきました。

github.com

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

ASP.NET Core 3.0 Preview 8 で gRPC に Azure AD 認証つけてみよう

$
0
0

さて、前回は簡単に呼び出す奴を作ってみました。

blog.okazuki.jp

今回は Azure AD 認証を付けたいと思います。

Azure AD にアプリの登録

では、Azure AD にアプリを登録します。サーバー側とクライアント側の 2 つを登録しましょう。

サーバーアプリの登録

とりあえずシングルテナント(自分のテナントのユーザーだけ)でさくっとサーバー側を作ります。

f:id:okazuki:20190903171712p:plain

サーバー側のアプリが作成されたら、スコープを追加します。「APIの公開」で 「Scope の追加」を選択して適当な名前で作ります。 まず、アプリケーション ID URL を作るように言われるので「保存してから続ける」を選択します。

f:id:okazuki:20190903171920p:plain

続けてスコープの追加です。適当に名前を入れて、同意できる人を管理者とユーザーにしてその他の項目も適当に埋めていきます。

f:id:okazuki:20190903172116p:plain

スコープの追加ボタンを押すとスコープが追加されます。スコープの api://アプリID/スコープ名は後で使うのでコピーしておきましょう。

クライアントアプリの登録

続けてクライアントのアプリを登録します。今回のクライアントアプリは .NET Core で作った WPF アプリです。 残念ながら各種ライブラリーが、まだ .NET Core には組み込みブラウザーなんてないと思って実装されてるので、そんな感じで登録します。

具体的にいうと、アプリ作成時(作成後でも編集できます)のリダイレクト URI で パブリッククライアント(モバイルとデスクトップ)を選択して http://localhostを設定します。

f:id:okazuki:20190903172444p:plain

アプリが出来たら「APIのアクセス許可」に行って「アクセス許可の追加」を選択します。API の選択画面になるので「自分の API」タブを選択して、先ほど作ったサーバー用アプリに作ったスコープを選択して「アクセス許可の追加」をします。

f:id:okazuki:20190903172809p:plain

後で使うので、クライアントアプリの「概要」ページにいって「アプリケーション(クライアント)ID」をコピーしておきます。

f:id:okazuki:20190903173043p:plain

テナント ID の取得

先ほどと同じアプリの「概要」ページから「ディレクトリ(テナント)ID」をコピーしておきます。

サーバーに認証機能を追加

では、サーバー側に認証機能を追加しましょう。 まず、以下の NuGet パッケージを入れます。

  • Microsoft.AspNetCore.Authentication.JwtBearer 3.0.0-preview8.xxxxx

そして、Startup.csに JWT トークンによる認証の設定を追加します。

using GrpcService.Services;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace GrpcServer
{
    publicclass Startup
    {
        publicvoid ConfigureServices(IServiceCollection services)
        {
            // これと
            services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
                .AddJwtBearer(options =>
                {
                    options.Authority = "https://login.microsoftonline.com/ここにテナントID/";
                    options.Audience = "api://サーバー側アプリのクライアントID";
                });
            services.AddAuthorization();

            services.AddGrpc();
        }

        publicvoid Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseRouting();

            // これを追加
            app.UseAuthentication();
            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapGrpcService<GreeterService>();
                endpoints.MapGet("/", async context =>
                {
                    await context.Response.WriteAsync("Hello World!");
                });
            });
        }
    }
}

ここの ConfigureServices で先ほどメモした値を使います。Authority はテナントIDを埋め込んで、Audience にサーバー側アプリのスコープを作成したときに作られた api://アプリIDの値を設定します。先ほど控えたスコープの値から最後の /Call.APIをとった値になります。

そして gRPC のサービスに Authorize 属性をつけます。ここら辺は REST API と同じ感じですね。

using System.Threading.Tasks;
using Grpc.Core;
using GrpcSample;
using Microsoft.AspNetCore.Authorization;

namespace GrpcService.Services
{
    [Authorize]
    publicclass GreeterService : Greeter.GreeterBase
    {
        publicoverride Task<GreetReply> Greet(GreetRequest request, ServerCallContext context)
        {
            return Task.FromResult(new GreetReply
            {
                Message = $"Hello {request.Name}",
            });
        }
    }
}

この状態でアプリを起動して API を呼ぼうとすると、認証エラーになります。

f:id:okazuki:20190903174408p:plain

因みに前回は Visual Studio Code で dotnet runで起動したので別によかったのですが Visual Studio だとデフォルトで IIS Express で起動しようとするので、これをやめてやる必要があります。以下のような感じで設定変更できます。

f:id:okazuki:20190903174134p:plain

クライアントにログイン機能を追加

では、クライアントにログイン機能を追加しましょう。MSAL.NET を追加します。パッケージ名は、Microsoft.Identity.Clientになります。 現時点での最新の 4.3.1 を入れました。

今回は簡単に実装するため全部 MainWindow.xaml.csに書いていきます。書き忘れたけどサーバー側も設定をハードコーディングしてるけど、ちゃんと設定から読むようにしてね。

using System;
using System.Net.Http;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;
using GrpcSample;
using Microsoft.Identity.Client;

namespace GrpcClient
{
    /// <summary>/// Interaction logic for MainWindow.xaml/// </summary>publicpartialclass MainWindow : Window
    {
        privatestring[] Scopes { get; } = new[] { "api://サーバーアプリのID/Call.API" }; // スコープ作った時にコピーしておいたやつprivatereadonly IPublicClientApplication _app;
        public MainWindow()
        {
            InitializeComponent();

            // 本当は設定(appsettings.json とか)から読む
            var options = new PublicClientApplicationOptions
            {
                ClientId = "クライアントアプリのID",
                RedirectUri = "http://localhost",
                TenantId = "テナントのID",
            };
            _app = PublicClientApplicationBuilder.CreateWithApplicationOptions(options).Build();
        }

        private async void CallGrpcServiceButton_Click(object sender, RoutedEventArgs e)
        {
            var accessToken = await GetAccessTokenAsync();

            using (var client = new HttpClient
            {
                BaseAddress = new Uri("https://localhost:5001")
            })
            {
                var greetServices = Grpc.Net.Client.GrpcClient.Create<Greeter.GreeterClient>(client);
                var response = await greetServices.GreetAsync(new GreetRequest
                {
                    Name = textBoxName.Text,
                },
                new Grpc.Core.Metadata
                {
                    { "Authorization", $"Bearer {accessToken}" },                
                });
                MessageBox.Show(response.Message);
            }
        }

        private async Task<string> GetAccessTokenAsync()
        {
            AuthenticationResult r;
            try
            {
                var account = (await _app.GetAccountsAsync())?.FirstOrDefault();
                r = await _app.AcquireTokenSilent(Scopes, account).ExecuteAsync();
            }
            catch (MsalUiRequiredException)
            {
                r = await _app.AcquireTokenInteractive(Scopes)
                    .WithSystemWebViewOptions(new SystemWebViewOptions
                    {
                        OpenBrowserAsync = SystemWebViewOptions.OpenWithChromeEdgeBrowserAsync,
                    })
                    .ExecuteAsync();
            }

            return r.AccessToken;
        }
    }
}

IPublicClientApplicationの作成をしているコンストラクターの処理と、アクセストークンの取得をしている GetAccessTokenAsyncあたりが注目かな。 そして、最後に gRPC の API を呼び出すメソッドで Grpc.Core.Metadataでお馴染みの Bearer でトークン渡してやる感じです。

では、実行してみましょう。

WPF のアプリでボタンを押すとブラウザーでログイン画面が開きます。アプリを作った Azure AD のテナントのユーザーでサインインしてください。 同意が求められるので逆らわずにいきましょう。

ログインに成功すると、ブラウザーは閉じていいよというメッセージが出ます。なので閉じて(ほっといてもいいけど)WPFアプリ側に戻ると gRPC の API が呼べてますね!やったね!

f:id:okazuki:20190903175948p:plain

認証情報にアクセスしたい

サーバー側で認証情報触りたい場合はメソッドの引数の ServerCallContextGetHttpContextを呼ぶと HttpContext が取れるので、それ経由でアクセスできます。例えば以下のような感じで

using System.Threading.Tasks;
using Grpc.Core;
using GrpcSample;
using Microsoft.AspNetCore.Authorization;

namespace GrpcService.Services
{
    [Authorize]
    publicclass GreeterService : Greeter.GreeterBase
    {
        publicoverride Task<GreetReply> Greet(GreetRequest request, ServerCallContext context)
        {
            var identity = context.GetHttpContext().User.Identity;
            return Task.FromResult(new GreetReply
            {
                Message = $"Hello {request.Name}, IsAuthorized: {identity.IsAuthenticated}, Your account is {identity.Name}",
            });
        }
    }
}

実行すると、以下のような感じになります。

f:id:okazuki:20190903180359p:plain

まとめ

Azure App Service でホスト出来るようになるのを首を長くして待ってます。

ソースコードは前回のリポジトリに addauth というブランチを切ってあるので見てください。

github.com

ASP.NET Core 3.0 + gRPC + WPF on .NET Core 3.0 で Azure AD を使って認証・認可

$
0
0

過去記事

blog.okazuki.jp

blog.okazuki.jp

本文

前回、認証だけはやりました。今回はユーザーの権限とかを見て何かしたいとか、この API は呼べる、呼べないを構成していきたいと思います。

Azure AD のグループ機能で認可してみよう

Azure AD のグループでユーザーをグループに所属させることが出来ます。私の環境ではグループ作る時にセキュリティと Office 365 が選択出来ますが今回はセキュリティの方で適当にグループを作ります。

f:id:okazuki:20190904193927p:plain

グループを作って適当にユーザーを所属させます。そしてグループを識別するためのオブジェクト IDを控えておきます。以下のようにグループの一覧にもちらっとオブジェクト IDが表示されますが

f:id:okazuki:20190904194040p:plain

グループを選択するとオブジェクト IDがコピーできます。

f:id:okazuki:20190904194608p:plain

グループが出来たので、次に Azure AD に登録したアプリをいじっていきます。アプリにわたっていくクレームにユーザーのセキュリティグループがわたるようにします。Azure AD の「アプリの登録」からサーバー側のアプリを開きます。そして「マニフェスト」を選択します。すると JSON が表示されます。 その中に ""groupMembershipClaims": null,という行があるので "groupMembershipClaims": "SecurityGroup",に変更して保存します。

f:id:okazuki:20190904195137p:plain

この状態でアプリを実行して適当なユーザーでログインして gRPC のサービスを呼び出してみましょう。gRPC のサービスのメソッドの中でブレークポイントで止めて HttpContext の中の User.Identity.Claims を覗いてみると groupsをキーにしてグループのオブジェクト ID がわたってきてることが確認できます。

f:id:okazuki:20190904195732p:plain

これの有無で API が呼べる・呼べないを制御出来れば良さそうです。試しに admins グループじゃないと呼べない処理を 1 つ追加してみます。

.protoファイルに GreetForAdminという名前のメソッドをはやします。

syntax = "proto3";

option csharp_namespace = "GrpcSample";

service Greeter {
    rpc Greet (GreetRequest) returns (GreetReply);
    rpc GreetForAdmin (GreetRequest) returns (GreetReply);
}

message GreetRequest {
    string name = 1;
}

message GreetReply {
    stringmessage = 1;
}

サーバー側のサービスの実装は以下のようにしてみました。管理者なので Hello と気軽に挨拶するのではなく Dear にしてみました。

publicoverride Task<GreetReply> GreetForAdmin(GreetRequest request, ServerCallContext context)
{
    var identity = context.GetHttpContext().User.Identity;
    return Task.FromResult(new GreetReply
    {
        Message = $"Dear {request.Name}, IsAuthorized: {identity.IsAuthenticated}, Your account is {identity.Name}",
    });
}

権限の高い人には媚を売っていくスタイル。では、これを admins グループの人のみが呼べるようにします。(今のままだと誰でも呼べる)

Startup.csConfigureServicesメソッドにある services.AddAuthorization();を以下のように変えます。

services.AddAuthorization(options =>
{
    options.AddPolicy("Admins", policy => policy.RequireClaim("groups", "admins グループのオブジェクト ID"));
});

Admins というポリシーを追加しています。ポリシーの中身はクレームの中に groups に admins のオブジェクト ID があるということです。 ポリシーの適用は、Authorize属性で行います。

[Authorize("Admins")]
publicoverride Task<GreetReply> GreetForAdmin(GreetRequest request, ServerCallContext context)
{
    var identity = context.GetHttpContext().User.Identity;
    return Task.FromResult(new GreetReply
    {
        Message = $"Dear {request.Name}, IsAuthorized: {identity.IsAuthenticated}, Your account is {identity.Name}",
    });
}

では、クライアント側にボタンを追加して GreetForAdmin を呼び出す処理を追加しましょう。 MainPage.xaml を編集してボタンを追加します。

<Windowx:Class="GrpcClient.MainWindow"xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"xmlns:d="http://schemas.microsoft.com/expression/blend/2008"xmlns:local="clr-namespace:GrpcClient"xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"Title="MainWindow"Width="800"Height="450"mc:Ignorable="d"><StackPanel><TextBox x:Name="textBoxName" /><Button Click="CallGrpcServiceButton_Click"Content="Call gRPC service" /><Button Click="CallGrpcServiceForAdminButton_Click"Content="Call gRPC service for Admin" /></StackPanel></Window>

クリックイベントハンドラーは以下のように GreetForAdminAsync メソッドを呼ぶようにします。

private async void CallGrpcServiceForAdminButton_Click(object sender, RoutedEventArgs e)
{
    var accessToken = await GetAccessTokenAsync();

    using (var client = new HttpClient
    {
        BaseAddress = new Uri("https://localhost:5001")
    })
    {
        var greetServices = Grpc.Net.Client.GrpcClient.Create<Greeter.GreeterClient>(client);
        var response = await greetServices.GreetForAdminAsync(new GreetRequest
        {
            Name = textBoxName.Text,
        },
        new Grpc.Core.Metadata
        {
            { "Authorization", $"Bearer {accessToken}" },
        });
        MessageBox.Show(response.Message);
    }
}

実行してみます。admins グループに所属してないユーザーでログインして Admin のほうのボタンを押すと例外が出ます。

f:id:okazuki:20190904200851p:plain

admins グループに所属しているユーザーでログインして Admin の方のボタンを押すとちゃんと処理が呼べます。

f:id:okazuki:20190904201332p:plain

クライアント側でログインユーザーのグループの判別

さて、実際のプログラムではログインユーザーの所属するグループに応じてボタンとかメニューの表示・非表示を制御することになると思います。 JWT トークンをパースすれば、その中にクレームが入っているのでそれでやります。

自前でパースしてもいいのですが System.IdentityModel.Tokens.Jwtというパッケージが公開されているので、それを使いましょう。

www.nuget.org

JwtSecuretyTokenクラスにトークンを渡してやればいい感じに扱えます。Claims プロパティから目的のクレームがあるか探してチェックしたりすればいい感じですね。 以下のようにすれば呼び出し前に admins に所属しているかどうか確認できます。

private async void CallGrpcServiceForAdminButton_Click(object sender, RoutedEventArgs e)
{
    var accessToken = await GetAccessTokenAsync();

    var jwt = new JwtSecurityToken(accessToken);
    var groupId = jwt.Claims.FirstOrDefault(x => x.Type == "groups")?.Value;
    if (groupId != "admins グループのオブジェクト ID")
    {
        MessageBox.Show("You are not a member of admins group, right? Please do not click this button.");
        return;
    }

    using (var client = new HttpClient
    {
        BaseAddress = new Uri("https://localhost:5001")
    })
    {
        var greetServices = Grpc.Net.Client.GrpcClient.Create<Greeter.GreeterClient>(client);
        var response = await greetServices.GreetForAdminAsync(new GreetRequest
        {
            Name = textBoxName.Text,
        },
        new Grpc.Core.Metadata
        {
            { "Authorization", $"Bearer {accessToken}" },
        });
        MessageBox.Show(response.Message);
    }
}

実行して admins グループにいないユーザーでサインインすると以下のように、ちゃんと呼び出し前にチェック出来てることがわかります。

f:id:okazuki:20190904202613p:plain

一般的な注意点ですが、クライアントサイドでの権限チェックは処理を呼び出す呼び出さないといったことや、要素の表示・非表示くらいにしようねってのがあります。本当に権限が必要な処理はサーバーサイドでも、きちんとチェックしましょう。

アプリでロールを管理しよう

グループはグループでいいんですが、これは Azure AD 全体に影響があるものになります。 部門単位でちょっと開発するアプリで、そこの設定変更は影響が大きすぎるので、アプリ内で適当にロールみたいなのを割り当てたいといったこともあります。

その場合は、以下のドキュメントにあるように、Azure AD でアプリ単位でロールを定義出来ます。

docs.microsoft.com

Azure AD の「アプリの登録」でサーバー側のアプリを開きます。そして、マニフェストを開きます。そして、appRolesに JSON 手書きでロールを定義していきます。硬派ですね…。

例えば Users と Admins みたいなロールを追加するなら以下のような感じです。id には GUID を指定しますが、これは PowerShell Core で New-Guidと打って生成するのが楽でした。

"appRoles": [{"allowedMemberTypes": ["User"
            ],
            "description": "Administrators.",
            "displayName": "Admins",
            "id": "fc998089-b40c-40c1-af3c-161382ac6422",
            "isEnabled": true,
            "lang": null,
            "origin": "Application",
            "value": "Admins"
        },
        {"allowedMemberTypes": ["User"
            ],
            "description": "Users.",
            "displayName": "Users",
            "id": "237d8777-e380-4b5d-bb4a-192c126640f5",
            "isEnabled": true,
            "lang": null,
            "origin": "Application",
            "value": "Users"
        }],

次に、ユーザーにこのロールを割り当てます。これは GUI でやれます。安心。やりかたは Azure AD の「エンタープライズ アプリケーション」を選択してサーバー側のアプリを開きます。

そして「ユーザーとグループ」を選択して「ユーザーの追加」を選択します。

f:id:okazuki:20190904203652p:plain

そうするとユーザー(複数人選択可能)とロール(先ほど JSON で手書き追加したやつ)が選択できるのでお好みな感じで割り当てます。 このように構成しておくと、http://schemas.microsoft.com/ws/2008/06/identity/claims/roleという ClaimType で Role に指定した Value が入ってきます。なので、今回は Admins じゃないとダメにしたいので、Startup.cs の ConfigureServices の AddAuthorization メソッドを以下のように書き換えます。

services.AddAuthorization(options =>
{
    options.AddPolicy("Admins", policy => 
        //policy.RequireClaim("groups", "admins グループのオブジェクト ID ")
        policy.RequireClaim("http://schemas.microsoft.com/ws/2008/06/identity/claims/role", "Admins") // 今度はこっちね
    );
});

クライアント側で JwtToken を解析した結果の Claim には Type が roles に入ってるので以下のように書き換える感じになります。

var jwt = new JwtSecurityToken(accessToken);
//var groupId = jwt.Claims.FirstOrDefault(x => x.Type == "groups")?.Value;//if (groupId != "admins グループのオブジェクト ID")//{//    MessageBox.Show("You are not a member of admins group, right? Please do not click this button.");//    return;//}

var roleName = jwt.Claims.FirstOrDefault(x => x.Type == "roles")?.Value;
if (roleName != "Admins")
{
    MessageBox.Show("You are not a member of admins group, right? Please do not click this button.");
    return;
}

これで、Azure AD のセキュリティグループで設定したときと同じように動きます。

まとめ

Azure AD が導入されているなら、これでログイン出来る社内アプリを作らない手はないので使ってみましょう。

ソースコードは、これまでの GitHub のリポジトリーに moreauth というブランチを作ってそこに上げてます。

GitHub - runceel/GrpcNetCoreSample at moreauth

Surface Pro ユーザーが Dell の New XPS 13 2-in-1 を買ってみた感想

$
0
0

買ってみたのは以下の New XPS 13 2-in-1 です。

Dell Cinema & HDRディスプレイ搭載のXPS 13インチ7390 2-in-1ノートパソコン | Dell 日本

構成は以下のような感じ

  • メモリ 32 GB
  • CPU Core i7
  • SSD 1TB
  • キーボード配列 US
  • 色は黒

買い替え前は Surface Pro 2017 年モデルです。

  • メモリ 16 GB
  • CPU Core i7
  • SSD 512 GB
  • キーボード配列 US
  • キーボードの色は赤

買い替え理由

最近 Docker や Visual Studio や Visual Studio Code や Web ブラウザー(意外とメモリ食いますよねブラウザー…)を立ち上げてるとメモリーを 13 GB 以上軽く食うようになったので 32 GB メモリーで、そんなに重くないマシンを探してました。 でも、32 GB メモリーがあるノートは一目見てゲーミング PC と思うようなごつい見た目と重さを兼ね備えてたので持ち運び用との自分にはちょっとハードでした…。

あと、ディスクも 512GB だと残り 100GB を切るようになってきたので少し心もとない気持ちになってました。

そんな中、1.33 kg で 32 GB メモリの 13 インチのマシンが出る!というのを黒澤さんに教えてもらいました。

1 TB モデルもあるということで出たら購入すると意思決定して購入。ちなみに Surface Pro は、新しい里親が見つかりました。

使ってみた感想ハード面

ベゼル狭い!!

写真が非常に見づらいのですが…

f:id:okazuki:20190910172732j:plain

画面サイズの割に本体が小さいのですが、その理由はこのベゼルの狭さ。 最初に開封したときに思ったより本体小さいな?って思ったのですが、本当にベゼル狭いので本体の小ささに対して画面が広いです。

キーボードにちょっと違和感

今まで 5 年くらい Surface Pro / Book を触ってきてたのですが、それに比べるとキーボードが広いです。 普通のアルファベットの部分だけでも、ちょっと思ったより広くて違和感を感じますが、これはまぁいい感じです。

一番の違和感は Home, End, Page up, Page down キーが微妙な位置にあると感じるところ。

まず、矢印キーの左右の部分に pg uppg dnがあるので、左右キーの上側を押す癖がある人は左右に移動しようとしてページが飛ぶことがあります。

home と end は F10, F11 の部分にあって、私は F1 ~ F12 キーを割と使います。 なので Fn + F10, Fn + F11 で home, end になるように設定してるので若干エディターで行頭・行末に行くのがめんどくさいです。

Surface シリーズだと Fn + 左矢印、Fn + 右矢印でやってたので慣れの問題ですが慣れるまで時間がかかりそうです。

あと、上のツイートにあるとおり一番右上のボタンが電源ボタン兼指紋センサーなので、全体的にその列のキーが左に寄ってるような印象です。まだ直観で F7 を押してカタカナに変換が出来ないです。

まぁ、ここら辺は慣れの問題であって Surface シリーズとキー配置が違うね!というところですね。

指紋センサー

今まで Surface は Windows Hello の顔認証だったのですが、XPS 13 2-in-1 は指紋認証になります。 たまに、Surface のころの癖で PC を開いて PC の画面を見ながら「俺の顔はちゃんと見えてるか?」って思ってログインするのを待ってることがありますが、指紋認証なのでログインは当然されないということがあります。

まぁ、これも些細な慣れの問題。ボタンに指を置けば OK なので。

外部インターフェースが USB Type C が 2 個 + α

という感じなので、持ち歩くアダプターなどは当然それに合わせたものが必要になります。 私は Macbook 用に USB Type C からのディスプレイ出力(HDMI、VGA)・有線LAN・USBへの変換してくれるものを持ってたのでそれを流用してます。

家は、ドックをセットで買ったので家に帰ったらドックから伸びてるケーブル(USB Type C)をさせばディスプレイも電源もいい感じになるので快適です。

ということで、USB Type C 関連グッズがあれば問題にならない感じでした。

ソフト面

Windows 10 Pro 選んだので、まぁ Windows ですね。 自分のメインが GPU を酷使しないような開発系ソフトを使う感じなのでメモリ 32 GB と CPU がいいことが条件でした。

その面では超満足。

f:id:okazuki:20190910180437p:plain

Visual Studio 2019、Visual Studio Code、Edge Preview(Chronium 版 Edge)、Microsoft Teams、メーラーなどの普段使いのアプリを立ち上げてる状態でこれなので、ここから更に追い Docker とかをしても余裕がありそうでとても素敵です。

各種ツールのインストールを Chocolatey でインストールしていく運用に変えてみたので XPS とは関係ないですが使い続けてみてどうなるのかが楽しみです。

ディスプレイは 4K なのですが、以下のように 175% で使ってます。

f:id:okazuki:20190910180824p:plain

Full HD のディスプレイを繋いでみると解像度のでかさがわかる。

f:id:okazuki:20190910180913p:plain

実際に画面全体のスクリーンショットをとってみるとこんな感じです。

f:id:okazuki:20190910181602p:plain

上の、この記事書いてる画面のフルHDのモニター小さくてウケる。

まとめ

メモリ 32 GB で持ち運びが苦じゃない程度の重さなのが最高!


Promise 対応していないコールバック形式のライブラリーを Promise にしたい

$
0
0

Node.js v8.1 で util.promisifyっていう関数が追加されてたんですね。 古き良き伝統にしたがったコールバック形式の関数を Promise を返す形にしてくれる。つまり await 出来るようになる!

nodejs.org

ということで、今時コールバック形式のライブラリなんて…と思ってたら azure-storageがそれでした。 ということで使ってみよう。

使い方は簡単。

const f = util.promisify(hogehoge);

のようにコールバック形式の関数を渡してやると Promise を返す関数になるので…

await f();

とすれば OK。

じゃぁこんな感じに使えるかな。

import express from'express';import util from'util';import azure from'azure-storage';interface EntityType {
    PartitionKey: azure.TableService.EntityProperty<string>;
    RowKey: azure.TableService.EntityProperty<string>;
    Value: azure.TableService.EntityProperty<string>;}// Azure ストレージエミュレーターにつなぐconst client = azure.createTableService('AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;DefaultEndpointsProtocol=http;BlobEndpoint=http://127.0.0.1:10000/devstoreaccount1;QueueEndpoint=http://127.0.0.1:10001/devstoreaccount1;TableEndpoint=http://127.0.0.1:10002/devstoreaccount1;');asyncfunction getAll(){await util.promisify(client.createTableIfNotExists).bind(client)('test');const query =new azure.TableQuery().where('PartitionKey eq ?','a').select('Value');// 本当は util.promisify(client.queryEntities<EntityType>) みたいにしたかった。この場合どうするのが正解なんだろうreturnawait util.promisify(client.queryEntities).bind(client)('test', query,nullasany)as 
        azure.TableService.QueryEntitiesResult<EntityType>;}const app = express();
app.get('/',async(req, res)=>{const result =await getAll();
    res.json(result.entries.map(x => x.Value._));});

app.listen(3000);

注意点は、関数内部の this をちゃんと bind で指定してあげることくらいかな?

適当にデータつっこんだテーブルを用意して実行して localhost:3000 にアクセスすると…

f:id:okazuki:20190911202826p:plain

返ってきた!!

実際使うとしたら都度都度 util.promisify するのではなく、都合のいい感じのラッパークラスを用意する感じかなぁ?

import express from'express';import util from'util';import azure from'azure-storage';interface EntityType {
    PartitionKey: azure.TableService.EntityProperty<string>;
    RowKey: azure.TableService.EntityProperty<string>;
    Value: azure.TableService.EntityProperty<string>;}// こういうラッパークラスにめんどくさい処理は押し込んでおいて…class TableService {constructor(private tableService: azure.TableService){}public createTableService = util.promisify(this.tableService.createTableIfNotExists).bind(this.tableService);private _queryEntities = util.promisify(this.tableService.queryEntities).bind(this);publicasync queryEntities<T>(tableName: string, query: azure.TableQuery, token: azure.TableService.TableContinuationToken){returnawaitthis._queryEntities(tableName, query, token)as
            azure.TableService.QueryEntitiesResult<T>;}}const innerClient = azure.createTableService('AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;DefaultEndpointsProtocol=http;BlobEndpoint=http://127.0.0.1:10000/devstoreaccount1;QueueEndpoint=http://127.0.0.1:10001/devstoreaccount1;TableEndpoint=http://127.0.0.1:10002/devstoreaccount1;');const client =new TableService(innerClient);asyncfunction getAll(){// 使う側は意識しなくてよくするawait client.createTableService('test');const query =new azure.TableQuery().where('PartitionKey eq ?','a').select('Value');returnawait client.queryEntities<EntityType>('text', query,nullasany);}const app = express();
app.get('/',async(req, res)=>{const result =await getAll();
    res.json(result.entries.map(x => x.Value._));});

app.listen(3000);

まとめ

ライブラリー側で await 出来るように Promise 返してくれるようにしてほしさある。

Visual Studio Code で CLI から今開いている VS Code でフォルダーを開くコマンド

$
0
0

今まで

$ code .

してたのですが、これだと新しいウィンドウが開いてしまっていました。-rオプションをつけると今あるものを再利用してくれるようです。 さらに --addオプションはワークスペースにフォルダーを追加してくれる。

ということなので、例えば sample1フォルダーと sample2フォルダーを作って git initでもした後に、VS Code で同じワークスペースとしてフォルダーを開くには以下のコマンドを打てばいい感じです。

$ mkdir sample1
$ cd sample1
$ git init
$ code -r .

# 一度画面がリフレッシュされるので VS Code のターミナルを開きなおして
$ cd ..
$ mkdir sample2
$ cd sample2
$ git init
$ code --add .

という感じでいいです。やってみた感じだと以下のような風になります。

f:id:okazuki:20190912171839g:plain

今までフォルダー開くダイアログ使ってたのがはかどることになりそう。

コマンドラインオプションのドキュメントはこちら。

code.visualstudio.com

Visual Studio Users Community #1 で「はじめよう Azure Functions」というタイトルで発表してきました

$
0
0

久しぶりにプライベートな活動として登壇してきました!!

www.slideshare.net

Azure Functions の開発してましたが、Azure ポータルを一回も開かなかったセッションでした。 今回紹介したものの中では SignalR Service はローカル版がないので実際にクラウドに作ってますが、こちらもフリープランがあったりします。

f:id:okazuki:20190914155820p:plain

開発してローカルで動かしてる限りは 0 円ではじめられるので是非是非セッションの目的であった Azure Functions いいなって思ってもらえたら試してみてください!

Visual Studio の場合

Visual Studio Installer で Azure にチェックを入れてインストール

Visual Studio Code の場合

以下のドキュメントあたりで環境の作り方があります。

docs.microsoft.com

プログラミング言語勉強用の環境を Visual Studio Code + Docker で手に入れてみる

$
0
0

Visual Studio Code を入れます。

azure.microsoft.com

Visual Studio のリモート開発の拡張機能を入れます。

marketplace.visualstudio.com

そして docker を入れます。

www.docker.com

Windows の人は入れたら設定からドライブ共有をオンにしておきましょう。

Python 3 の環境が欲しい

適当なフォルダーを Visual Studio Code で開きます。 F1Ctrl + Shift + Pあたりでコマンドパレットを出して Remote Addあたりで検索すると Remote Containers: Add Development Container Configuration Files...という項目が出てきます。

f:id:okazuki:20190915211841p:plain

どんな開発環境が欲しいのかリストが出てくるので Python 3 を選びましょう。ファイルがいくつか追加されて以下のようなものが表示されるので Reopen in Containerを選択します。

f:id:okazuki:20190915212122p:plain

初回は docker のビルドが走るのでしばらく待ってると…、Python の入ったコンテナーで先ほどのフォルダーが開かれます。

f:id:okazuki:20190915212347p:plain

ハローワールドしてみましょう

f:id:okazuki:20190915212453p:plain

F5 を押して Python File を選択すると実行されます。

f:id:okazuki:20190915212530p:plain

ブレークポイントを置いてると…ちゃんと止まります!!

f:id:okazuki:20190915212602p:plain

ホストには Python 入ってないというのを示そうとコマンドプロンプトで python って打ち込んだらストアが開いてびっくりした。

f:id:okazuki:20190915212758p:plain

再度開くときは Remote Containers: Open Folder in Containerあたりから開けばいい感じになります。

f:id:okazuki:20190915213002p:plain

フォルダーダイアログがだるい場合は…

$ cd 目的のフォルダー
$ code -r .

あとはコンテナーで Reopen しますかと聞かれるので Reopen してもらうだけです。

まとめ

Docker があれば割と何でもできて便利。 VS Code でシームレスに使えるところが神。

docker 上に Vue.js + TypeScript の開発環境を整えて Visual Studio Code で開く

$
0
0

とりあえず新しいマシンにして docker が快調に動くようになったので少し色々試してみてます。昨日は Python の環境で今日は Vue.js で試してみました。

blog.okazuki.jp

これのいいところは、誰かが環境整えたら他の人は VS Code で開くだけで同じ環境で開発できちゃうところですよね。

環境作り

とりあえず、適当な空のフォルダーを作って、そこにリモート開発の設定を追加します。何かベースにいいのがないかなと探してたら node.js LTS + TypeScript がありました。君に決めた。

f:id:okazuki:20190916125839p:plain

生成された Dockerfile に Vue CLI を入れるコマンドを一行追加します。 tslint と typescript を入れている次の行くらいに入れておきました。

# Install tslint and typescript globally
&& npm install -g tslint typescript \
&& npm install -g @vue/cli \

devcontainer.jsonnpm run serveしたときに起動する 8080ポートを追加します。

"appPort": [8080],

あと、Vetur 拡張機能も Visual Studio Code に入れておかないと話にならないので入れる設定をします。devcontainer.jsonの extensions を以下のような感じにします。

"extensions": ["ms-vscode.vscode-typescript-tslint-plugin",
        "octref.vetur"
]

全体はこんな感じになりました。

// For format details, see https://aka.ms/vscode-remote/devcontainer.json or the definition README at// https://github.com/microsoft/vscode-dev-containers/tree/master/containers/typescript-node-lts{"name": "Node.js (latest LTS) & TypeScript",
    "dockerFile": "Dockerfile",
    "appPort": [8080],
    // Use 'settings' to set *default* container specific settings.json values on container create. // You can edit these settings after create using File > Preferences > Settings > Remote."settings": {"terminal.integrated.shell.linux": "/bin/bash"
    },
    // Uncomment the next line if you want to publish any ports.// "appPort": [],// Uncomment the next line to run commands after the container is created.// "postCreateCommand": "yarn install",// Uncomment the next line to use a non-root user. On Linux, this will prevent// new files getting created as root, but you may need to update the USER_UID// and USER_GID in .devcontainer/Dockerfile to match your user if not 1000.// "runArgs": [ "-u", "node" ],// Add the IDs of extensions you want installed when the container is created in the array below."extensions": ["ms-vscode.vscode-typescript-tslint-plugin",
        "octref.vetur"
    ]}

出来たら Reopen in container のコマンドをコマンドパレットから実行すると docker build が走ります。

チーム開発とかで使う場合は、何処か適当なリポジトリーに @vue/cli とかまで入れたイメージ作っておいて、それをベースにするほうが各人が全部ビルドしなくていいし各種ツールのバージョンもしっかりそろっていい感じかなぁって思ってます。

ビルドが終わって開かれたので Terminal で vue と打ち込むとちゃんと入ってます!

f:id:okazuki:20190916131243p:plain

とりあえずカレントのディレクトリーにプロジェクト作ってみる感じで以下のようにうってみましょう。

$ vue create .

お好みのオプションを選んでさくっとプロジェクトを作ると、ちゃんとできた!!(当然ですけど) Explorer 上でもモリモリファイル出来ていくのが面白い

f:id:okazuki:20190916131721p:plain

そして npm run serveうったら http://localhost:8080に Windows 側からブラウザーで開きます。先ほど devcontainer.json8080ポートの設定を追加しておいたので 8080ポートは自動的に docker コンテナ内に転送されます。

インテリセンスも効くのでいいですね!!

f:id:okazuki:20190916132007p:plain

まとめ

普通に普段から node.js 使ったり vue.js やってるならローカルに入れるのが一番いいですがプロジェクトで全体的に環境整えたいとか、ちょっと興味があってやってみたいんだけどローカルに入れるのはなんかやだなぁっていうのには Visual Studio Code のリモート開発機能で Docker 上に開発環境作って開くのとてもよさそうですね。

Viewing all 1388 articles
Browse latest View live


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