20200727のC#に関する記事は6件です。

宇宙船のコンピュータの整数演算ユニット作成し、船員を助けよう!

宇宙船のコンピュータの整数演算ユニット作成し、船員を助けよう!!

illustrain09-utyuu5.png

問題

以下の入力の場合、実装して、出力の配列[0]の値を示せ。

入力

[1,1,28,3,1,1,2,3,1,3,4,3,1,5,0,3,2,9,1,19,1,9,19,23,1,23,5,27,2,27,10,31,1,6,31,35,1,6,35,39,2,9,39,43,1,6,43,47,1,47,5,51,1,51,13,55,1,55,13,59,1,59,5,63,2,63,6,67,1,5,67,71,1,71,13,75,1,10,75,79,2,79,6,83,2,9,83,87,1,5,87,91,1,91,5,95,2,9,95,99,1,6,99,103,1,9,103,107,2,9,107,111,1,111,6,115,2,9,115,119,1,119,6,123,1,123,9,127,2,127,13,131,1,131,9,135,1,10,135,139,2,139,10,143,1,143,5,147,2,147,6,151,1,151,5,155,1,2,155,159,1,6,159,0,99,2,0,14,0];

出力

入力配列が同じサイズの配列

入力:[1,0,0,0,99] 。出力: [2,0,0,0,99]    原因 (1 + 1 = 2).
入力:[2,3,0,3,99]。出力:[2,3,0,6,99]       原因(3 * 2 = 6).
入力:[2,4,4,5,99,0]。出力:[2,4,4,5,99,9801]  原因(99 * 99 = 9801).
入力:[1,1,1,4,99,5,6,0,99] 。出力:[30,1,1,4,2,5,6,0,99]
制約

なし。言語は限らない。

ボーナス

出力配列[0]が19690720となる場合の入力配列[1]と[2]の値は何番でしょう。
取りうる範囲は0~99とする。

回答

回答例1(JavaScript)
const intCodeComputer = (intCode=[99]) => {
   for(let i=0; i<intCode.length; i+=4){
       if(intCode[i] === 99) return intCode;
       switch(intCode[i]) {
           case 1:
               intCode[intCode[i+3]] = intCode[intCode[i+1]] + intCode[intCode[i+2]];
               break;
           case 2:
               intCode[intCode[i+3]] = intCode[intCode[i+1]] * intCode[intCode[i+2]];
               break;
           default: break;
       }
   }
};

const answer = intCodeComputer([...PUZZLE_INPUT]);
console.log(answer[0]);
回答例2(C#)
static void Main(string[] args)
{
   Console.WriteLine(string.Join(',', OperationRecursive(args, 0)));
}

private static int[] OperationRecursive(int[] sources, int step)
{
   int calc(Func<int, int, int> f, int x, int y) => f(x, y);

   var targets = sources.Skip(step * 4).Take(4).ToArray();

   if (targets[0] == 99)
       return sources;
   if (targets[0] == 1)
       sources.SetValue(calc((x, y) => x + y, sources[targets[1]], sources[targets[2]]), targets[3]);
   if (targets[0] == 2)
       sources.SetValue(calc((x, y) => x * y, sources[targets[1]], sources[targets[2]]), targets[3]);

   return OperationRecursive(sources, step + 1);
}
回答例3(GO)
func main() {
 numlist := []int{}
 for i := 0; i < len(numlist); i += 4 {
   var a = numlist[i+1]
   var b = numlist[i+2]
   var c = numlist[i+3]

   if numlist[i] == 1 {
     var d = numlist[a] + numlist[b]
     numlist[c] = d
   }

   if numlist[i] == 2 {
     var d = numlist[a] * numlist[b]
     numlist[c] = d
   }

   if numlist[i] == 99 {
     break
   }
 }
 fmt.Println(numlist[0])
}
回答例4(Java)
public class Main {
   public static void main(String[] args) {
       int [] arrayCode = {};
       for(int i=0;i<arrayCode.length; i+=4)
       {
           if(arrayCode[i]==99){
               break;
           }

           if(arrayCode[i]==1)
           {
               arrayCode[arrayCode[i+3]] = arrayCode[arrayCode[i+1]] + arrayCode[arrayCode[i+2]];
           }
           else if(arrayCode[i]==2)
           {
             arrayCode[arrayCode[i+3]] = arrayCode[arrayCode[i+1]] * arrayCode[arrayCode[i+2]];
           }
       }
       System.out.print(arrayCode[0]);
   }
}
回答例5(Perl)
my @array = (1,0,0,0,99);

sub calc {
   my @array = @_;
   my $length = @array;

   for (my $i = 0; $i < $length; $i+=4) {
       ($array[$i] == 99) && (last);

        if ($array[$i] == 1) {
            $array[$array[$i+3]] = $array[$array[$i+1]] + $array[$array[$i+2]]
        } elsif ($array[$i] == 2) {
           $array[$array[$i+3]] = $array[$array[$i+1]] * $array[$array[$i+2]]
        }
   }
   return $array[0];
}

$result = &calc(@array);

print "$result";
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

MVVM をリアクティブプログラミングで快適に ReactiveProperty オーバービュー 2020 年版 前編

前に書いた記事が 2015 年のものだったので 2020 年用に書き直していこうと思います。ReactiveProperty 7.1.0 を想定して書いています。また、WPF か UWP か Xamarin.Forms での MVVM 開発の基本的な知識と Reactive Extensions の基本的な知識があることを前提に記載しています。

ReactiveProperty とは

ReactiveProperty は Cysharp の @neuecc さんが開発したライブラリになります。
現在も、UniRx や UniTask などでの成果をたまにバックポートするプルリクエストをくれます。多謝。

GitHub でソースコードは公開されています。

WPF や UWP や Xamarin.Forms などの XAML を使用するクライアント サイドのアプリケーション開発でよく採用される MVVM パターンをサポートする機能やリアクティブ プログラミングとシームレスに繋ぐ機能が含まれています。主に XAML 系プラットフォームで MVVM を採用するときに省力化のために採用されることが多い ReactiveProperty ですが、特に MVVM の何かに依存しているクラスライブラリではないので、利用しようと思えば何処でも利用可能なライブラリになります。

コア機能のターゲットフレームワークは .NET Standard 2.0 で、その他に WPF や UWP などのプラットフォーム固有機能は、それぞれのプラットフォーム向けのパッケージが提供されています。

提供している NuGet パッケージ

Package Id Version and downloads Description
ReactiveProperty ReactiveProperty の基本的な機能が入ったパッケージ。基本的にはこのパッケージを参照する。
ReactiveProperty.Core Reactive Extensions に依存していない ReactivePropertySlim や ReadOnlyReactivePropertySlim が含まれます。Rx は使わないけど INotifyPropertyChanged の実装の省力化のために ReactiveProperty を使いたいケースではこちらを参照してください。
ReactiveProperty.WPF EventToReactiveProperty や EventToReactiveCommand が含まれます。.NET Framework 4.6.1 や .NET Core 3.0 以降で利用可能です。
ReactiveProperty.UWP UWP 用のEventToReactiveProperty や EventToReactiveCommand が含まれます。
ReactiveProperty.XamarinAndroid Xamarin Android 向け(Forms 向けではないです)の各種拡張メソッドが含まれています。
ReactiveProperty.XamariniOS Xamarin iOS 向け(Forms 向けではないです)の各種拡張メソッドが含まれています。

参照すべきパッケージ

  • WPF の開発
    • ReactiveProperty.WPF (EventToReactiveCommand, EventToReactiveProperty を使用する場合)
    • ReactiveProperty (EventToReactiveCommand, EventToReactiveProperty が必要ない場合)
  • UWP の開発
    • ReactiveProperty.UWP (EventToReactiveCommand, EventToReactiveProperty を使用する場合)
    • ReactiveProperty (EventToReactiveCommand, EventToReactiveProperty が必要ない場合)
  • Xamarin.Forms の開発
    • ReactiveProperty
  • Xamarin Android の開発
    • ReactiveProperty.XamarinAndroid
  • Xamarin iOS の開発
    • ReactiveProperty.XamariniOS

基本機能

ReactiveProperty の基本機能は ReactiveProperty クラスと ReactivePropertySlim クラスになります。ReactiveProperty クラスと ReactivePropertySlim クラスは INotifyPropertyChanged インターフェースと IObservable インターフェースを実装した Value プロパティを持つクラスです。
以下のように使用できます。

var rp = new ReactivePropertySlim<string>("初期値");
Console.WriteLine(rp.Value); // 初期値
rp.Value = "別の値を設定";
Console.WriteLine(rp.Value); // 別の値を設定

INotifyPropertyChanged インターフェースを持っているので PropertyChanged イベントを購読することで値に変更があったときに処理が行えますが、これは WPF や UWP などのデータバインディングで使用するためのものなので、値に変更があったときの処理は Reactive Extensions のメソッドを使用して行います。

Subscribe メソッドを使うことで、簡単に値に変更があったときの処理が行えます。

var rp = new ReactivePropertySlim<string>("初期値");
rp.Subscribe(x => Console.WriteLine($"Subscribe: {x}")); // Subscribe: 初期値
rp.Value = "別の値"; // Subscribe: 別の値

MVVM パターンのアプリケーションでは、プロパティに変更があったことを INotifyPropertyChanged インターフェースを実装して PropertyChanged イベントを発行することで View に伝えます。そのため、ViewModel クラスに INotifyPropertyChanged を実装してプロパティに変更通知の機能を実装します。
この変更通知のコードは以下のように同じようなコードの実装の繰り返しなので非常に冗長になります。

private string _name;
public string Name
{
  get => _name;
  set 
  {
    _name = value;
    PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Name)));
  }
}

基本クラスの導入などで若干省力化は出来ますが、書いていてあまり楽しいコードではありません。ReactiveProperty を使うとプロパティの定義は以下のようになります。この省力化が出来ることが ReactiveProperty の導入の 1 つの動機になります。

public ReactivePropertySlim<string> Name { get; } = new ReactivePropertySlim<string>();

XAML 側では以下のようにバインドします。上が普通の書き方で、下が ReactiveProperty の場合の書き方になります。

<!-- 普通のプロパティへのバインド -->
<TextBlock Text="{Binding Name}" />
<!-- ReactiveProperty へのバインド -->
<TextBlock Text="{Binding Name.Value}" />

ReactiveProperty を使った場合は実際の値を持っているのは ReactiveProperty の Value プロパティなので .Value が Binding のパスに必要になります。

Slim と無印の違い

ReactiveProperty には ReactiveProperty クラスと ReactivePropertySlim クラスの 2 種類のクラスがあります。この後紹介する読み取り専用の ReadOnlyReactiveProperty も Slim と無印があります。

原則としては Slim がついている方で良いケースではなるべく Slim を使うことを想定しています。Slim のほうが機能が少ないですが Slim のほうが性能が桁違いに良いです。

Slim との機能面での差異は後程説明します。

Reactive Extensions との統合

ReactiveProperty には ReadOnlyReactiveProperty や ReadOnlyReactivePropertySlim というクラスがあり、これは IObservable から作成できます。読み取り専用のプロパティとして動作する点が特徴です。
例えば 1 秒ごとに 1, 2, 3, 4, ... のようにカウントアップしていく IObservable<int> の変数 ox に対して ToReadOnlyReactivePropertyToReadOnlyReactivePropertySlim を呼び出すことで作成できます。この場合 IObservable<int> から新しい値が発行されると Value プロパティの値が更新される ReadOnlyReactivePropertyReadOnlyReactivePropertySlim が作成されます。

コード例を以下に示します。

var subject = new Subject<int>(); // IObservable<int> の実装
var rp = subject.ToReadOnlyReactivePropertySlim(0); // 初期値 0 で作成
Console.WriteLine(rp.Value); // 0

// 100 を発行
subject.OnNext(100);
Console.WriteLine(rp.Value); // Value の値が 100 になる

// 9999 を発行
subject.OnNext(9999);
Console.WriteLine(rp.Value); // Value の値が 9999 になる

ReactiveProperty や Reactive Extensions を使うと様々なものが IObservable として扱えます。つまり、様々なものを ReadOnlyReactiveProperty/ReadOnlyReactivePropertySlim にすることが出来ます。

ReactiveProperty は IObservable

ReactiveProperty や ReactivePropertySlim や ReadOnlyReactiveProperty や ReadOnlyReactivePropertySlim は全て IObservable として扱えます。そのため以下のように Reactive Extensions の Select などのメソッドを使って加工した ReactiveProperty の値を ReadOnlyReactiveProperty に変換できます。

例えば文字を大文字に変換して ReadOnlyReactiveProperty に変換する処理は以下のようになります。

var rp = new ReactivePropertySlim<string>("okazuki"); // 初期値 okazuki
var rrp = rp.Select(x => x.ToUpper()).ToReadOnlyReactivePropertySlim(); // 大文字にして ReadOnlyReactiveProperty に変換

Console.WriteLine($"rp.Value = {rp.Value}, rrp.Value = {rrp.Value}"); // rp.Value = okazuki, rrp.Value = OKAZUKI

// 値を更新
rp.Value = "xin9le";
Console.WriteLine($"rp.Value = {rp.Value}, rrp.Value = {rrp.Value}"); // rp.Value = xin9le, rrp.Value = XIN9LE

イベントのようにアプリが動いている間に、任意のタイミングで何かが起きるようなケースのものも IObservable と扱うことが出来ます。例えば INotifyPropertyChanged インターフェースの PropertyChanged イベントを IObservable として扱うための ObserveProperty 拡張メソッドが Reactive.Bindings.Etensions 名前空間に定義されています。ObserveProperty でラムダ式でプロパティを指定すると、そのプロパティの変更があるたびに値が発行される IObservable になります。それに対して ToReadOnlyReactiveProperty を呼ぶことで ReadOnlyReactiveProperty が生成されます。

コード例を以下に示します。

using Reactive.Bindings;
using Reactive.Bindings.Extensions;
using System;
using System.ComponentModel;

namespace ConsoleApp6
{
    class Program
    {
        static void Main(string[] args)
        {
            var p = new Person();

            // Name プロパティを IObservable にして ReadOnlyReactiveProperty に変換
            var name = p.ObserveProperty(x => x.Name)
                .ToReadOnlyReactivePropertySlim();
            Console.WriteLine($"name.Value = ${name.Value}"); // 空文字

            // 元の値が変わると ReadOnlyReactiveProperty の Value も更新される
            p.Name = "okazuki";
            Console.WriteLine($"name.Value = ${name.Value}"); // okazuki
        }
    }

    // INotifyPropertyChanged の実装クラス
    class Person : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;
        private string _name;
        public string Name
        {
            get => _name;
            set
            {
                _name = value;
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Name)));
            }
        }
    }
}

ReactiveProperty を使う上での一つの勘所として、IObservable に変換できるものを見極めて ReadOnlyReactiveProperty に変換するというのがあります。

UI スレッドへのイベントの自動ディスパッチ (Slim には無い機能)

ReactiveProperty と ReadOnlyReactiveProperty は、Value プロパティが変わったときの PropertyChanged イベントを自動で UI スレッドで発行する機能があります。
WPF や UWP や Xamarin.Forms では、UI スレッド以外で PropertyChanged イベントを発行するとアプリケーションがクラッシュする可能性があるので自動で UI スレッドでイベントが発行してくれる機能は便利な側面があります。

ただし以下のようなデメリットがあります。

  • UI スレッドでイベントを発行するためにスレッドの切り替えが行われるので Value が変化しても直後に PropertyChanged イベントは起きない
    • Value を書き換えた直後に PropertyChanged で行ってる処理が完了していることを想定しているコードはうまく動かない
  • UI スレッドが複数あるケースでは破綻する
    • WPF や UWP では複数の UI スレッドがあるケースがあるので、この場合 UI スレッドが 1 つしかない想定の ReactiveProperty を使うとエラーが頻発するので使用しないでください。必ず Slim を使うか、後述する回避策を使用してください

ReactiveProperty のスレッド切り替えは ReactivePropertyScheduler.SetDefault メソッドで指定が可能です。指定しない場合は自動的にアプリケーションの UI スレッドに切り替えるスケジューラーが指定されます。デフォルトの挙動は以下のようになります。

WPF アプリケーションのボタンクリックイベントで以下のようなコードを書くとデバッグコンソールに以下のように表示されます。

クリックイベント
var latestValue = "";
var rp = new ReactiveProperty<string>();
rp.PropertyChanged += (s, e) => latestValue = rp.Value;
rp.Value = "okazuki";
Debug.WriteLine($"{rp.Value}, {latestValue}");
rp.Value = "xin9le";
Debug.WriteLine($"{rp.Value}, {latestValue}");
デバッグコンソール
okazuki, 
xin9le, 

この挙動を変えるにはアプリケーションの起動時の処理で ReactiveProperty が使用するスケジューラーを置き換えます。例えば WPF では App クラスの Startup イベントで置き換えが出来ます。以下のコードは即座に実行する ImmediateScheduler を設定しています。

App.xaml.cs
public partial class App : Application
{
    private void Application_Startup(object sender, StartupEventArgs e)
    {
        ReactivePropertyScheduler.SetDefault(ImmediateScheduler.Instance);
    }
}

この変更をした後に実行すると以下のような内容がデバッグコンソールに表示されます。

デバッグコンソール
okazuki, okazuki
xin9le, xin9le

アプリケーションが単一の UI スレッドであることが保証されている場合は便利な機能ですが、挙動などを把握していないと意図したとおりに動かないと感じる原因になるので注意してください。

入力値のバリデーション (Slim には無い機能)

ReactiveProperty には、入力された値のバリデーションを行う機能があります。ReactiveProperty の SetValidateNotifyError メソッドで入力値のチェック処理が書けます。エラーがある場合はエラーメッセージを返して、エラーがない場合は null を返します。

例えば空文字の入力を許可しない ReactiveProperty は以下のようになります。

MainWindowViewModel.cs
using Reactive.Bindings;
using System.ComponentModel;

namespace WpfApp1
{
    public class MainWindowViewModel : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        public ReactiveProperty<string> Name { get; }

        public MainWindowViewModel()
        {
            Name = new ReactiveProperty<string>()
                .SetValidateNotifyError(x => string.IsNullOrEmpty(x) ? "空文字はダメ" : null);
        }
    }
}

これを WPF の Window の DataContext に設定して TextBox とバインドします。

MainWindow.xaml
<Window x:Class="WpfApp1.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:WpfApp1"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Window.DataContext>
        <local:MainWindowViewModel />
    </Window.DataContext>
    <StackPanel Margin="10">
        <TextBox Text="{Binding Name.Value, UpdateSourceTrigger=PropertyChanged}"
                 ToolTip="{Binding Path=(Validation.Errors)/ErrorContent, RelativeSource={RelativeSource Self}}"/>
    </StackPanel>
</Window>

そうすると、以下のようにエラー時には赤枠が表示されて ToolTip でエラーメッセージが表示されます。

image.png

ウィンドウの初期表示の段階でバリデーションエラーが出ていると見た目よろしくないので初回のバリデーションエラーをスキップする機能も提供しています。
ReactiveProperty のコンストラクタや ToReactiveProperty の mode で ReactivePropertyMode で指定可能です。列挙型で | 演算子で複数指定できます。デフォルト値は ReactivePropertyMode.Default なので、それに加えて ReactivePropertyMode.IgnoreInitialValidationError を追加します。以下のようなコードになります。

MainWindowViewModel.cs
using Reactive.Bindings;
using System.ComponentModel;

namespace WpfApp1
{
    public class MainWindowViewModel : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        public ReactiveProperty<string> Name { get; }

        public MainWindowViewModel()
        {
            Name = new ReactiveProperty<string>(mode: ReactivePropertyMode.Default | ReactivePropertyMode.IgnoreInitialValidationError)
                .SetValidateNotifyError(x => string.IsNullOrEmpty(x) ? "空文字はダメ" : null);
        }
    }
}

初回実行時には、バリデーションエラーが起きません。

image.png

何かを入力して、再度空文字にするとエラーが表示されます。

image.png

また、バリデーションは DataAnnotations で指定可能です。DataAnnotations には必須入力を表す Required や特定の値の範囲にあることをチェックする Range や正規表現にマッチするかどうかをチェックする RegulerExpression など様々な属性が提供されています。
必要があれば独自のチェックロジックの属性を作成可能です。

ReactiveProperty に対して属性をつけて SetValidateAttribute メソッドを使ってラムダ式で対象の属性がついたプロパティを指定することでバリデーションが有効になります。

public class MainWindowViewModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    // 属性を指定する
    [Required(ErrorMessage = "名前は必須入力項目です")]
    public ReactiveProperty<string> Name { get; }

    public MainWindowViewModel()
    {
        // SetValidateAttribute メソッドで属性によるチェックを行うことを指定する
        Name = new ReactiveProperty<string>()
            .SetValidateAttribute(() => Name);
    }
}

バリデーションエラーのメッセージは、ObserveValidationErrorMessage メソッドで IObservable<string> として取得できます。そのため以下のようなコードを書くことでエラーメッセージをハンドリングできます。

using Reactive.Bindings;
using System;

namespace ConsoleApp2
{
    class Program
    {
        static void Main(string[] args)
        {
            var rp = new ReactiveProperty<string>()
                .SetValidateNotifyError(x => string.IsNullOrEmpty(x) ? "必須だよ" : null);
            rp.ObserveValidationErrorMessage()
                .Subscribe(x => Console.WriteLine($"エラーメッセージが変わったよ {x}"));

            rp.Value = "okazuki";
            rp.Value = null;
        }
    }
}

実行結果は以下のようになります。

エラーメッセージが変わったよ 必須だよ
エラーメッセージが変わったよ
エラーメッセージが変わったよ 必須だよ

最初のほうで ReactiveProperty は IObservable から作れるという説明をしました。なので、ObserveValidationErrorMessage の結果に対して ToReadOnlyReactivePropertySlim などを呼び出すとエラーメッセージを保持する ReadOnlyReactivePropertySlim などを作ることができます。
これを使って好きな場所にエラーメッセージを表示することができます。

ReactiveProperty のモード

バリデーションでも登場しましたが、ReactiveProperty, ReactivePropertySlim, ReadOnlyReactiveProperty, ReadOnlyReactivePropertySlim の生成時に mode 引数で挙動を若干カスタマイズ可能です。

mode は ReactivePropertyMode 列挙型で以下の値を指定できます。

  • ReactivePropertyMode.DistinctUntilChanged
    • Value に同じ値が設定された場合 OnNext や PropertyChanged を発行しない
  • ReactivePropertyMode.RaiseLatestValueOnSubscribe
    • Subscribe をしたときに現在の値で OnNext を呼び出す
  • ReactivePropertyMode.Defult
    • 何も指定しないときのデフォルト値。DistinctUntilChanged と RaiseLatestValueOnSubscribe を指定したのと同じ。
  • ReactivePropertyMode.IgnoreInitialValidationError
    • 最初のバリデーションエラーを無視する
  • ReactivePropertyMode.None
    • 特に何も指定しない

上記の値は | 演算子で区切って指定可能です。

C# のクラスとの接続 (Slim にはない機能)

ReactiveProperty は、普通の C# のクラスのプロパティをもとにして作ることが出来ます。

双方向同期

C# のクラスが INotifyPropertyChanged インターフェースを実装している場合は ToReactivePropertyAsSynchronized メソッドを使って特定のプロパティと同期した ReactiveProperty が生成されます。

using Reactive.Bindings.Extensions;
using System;
using System.ComponentModel;

namespace RxPropLab
{
    class Program
    {
        static void Main(string[] args)
        {
            var person = new Person();

            // 同期するプロパティを指定して ReactiveProperty を生成する
            var rp = person.ToReactivePropertyAsSynchronized(x => x.Name);

            // ReactiveProperty を更新すると
            rp.Value = "okazuki";
            // 生成元のオブジェクトのプロパティも更新されます
            Console.WriteLine(person.Name); // okazuki

            // 生成元のオブジェクトのプロパティが更新されると
            person.Name = "xin9le";
            // ReactiveProperty の値も更新されます
            Console.WriteLine(rp.Value); // xin9le
        }
    }

    class Person : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;
        private string _name;
        public string Name
        {
            get => _name;
            set
            {
                _name = value;
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Name)));
            }
        }
    }
}

ToReactivePropertyAsSynchronized メソッドには、プロパティの値を変換するロジックを含めることが出来ます。

using Reactive.Bindings.Extensions;
using System;
using System.ComponentModel;

namespace RxPropLab
{
    class Program
    {
        static void Main(string[] args)
        {
            var person = new Person();

            // 同期するプロパティを指定して ReactiveProperty を生成する
            var rp = person.ToReactivePropertyAsSynchronized(x => x.Name,
                x => x?.ToUpper(), // Person クラスから ReactiveProperty に行く途中で大文字に変換する
                x => x?.ToLower()); // ReactiveProperty から Person クラスに行く途中で小文字に変換する

            // ReactiveProperty を更新すると
            rp.Value = "OKAZUKI";
            // 生成元のオブジェクトのプロパティは小文字に変換された値が設定されます
            Console.WriteLine(person.Name); // okazuki

            // 生成元のオブジェクトのプロパティが更新されると
            person.Name = "xin9le";
            // ReactiveProperty の値も更新されて、大文字に変換されます
            Console.WriteLine(rp.Value); // XIN9LE
        }
    }

    class Person : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;
        private string _name;
        public string Name
        {
            get => _name;
            set
            {
                _name = value;
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Name)));
            }
        }
    }
}

この変換処理は単純なラムダ式による変換だけでなく、IObservable 型を受け取る変換ロジックを使うことで変換処理の他に任意の条件でのフィルターや、別の IObservable との合成などが可能になります。

以下の例は特定の値が来た場合に ReactiveProperty から Person への反映を行わないようにした例になります。

using Reactive.Bindings.Extensions;
using System;
using System.ComponentModel;
using System.Linq;
using System.Reactive.Linq;
using System.Security.Cryptography.X509Certificates;

namespace RxPropLab
{
    class Program
    {
        static void Main(string[] args)
        {
            var person = new Person();

            // 同期するプロパティを指定して ReactiveProperty を生成する
            var rp = person.ToReactivePropertyAsSynchronized(x => x.Name,
                (IObservable<string> ox) => ox.Select(x => x?.ToUpper()), // Person クラスから ReactiveProperty に行く途中で大文字に変換する
                // ReactiveProperty から Person クラスに行く途中で小文字に変換するが xin9le の場合は Person クラスに戻さない
                (IObservable<string> ox) => ox.Where(x => x != "xin9le").Select(x => x?.ToLower()));

            // ReactiveProperty を更新すると
            rp.Value = "OKAZUKI";
            // 生成元のオブジェクトのプロパティは小文字に変換された値が設定されます
            Console.WriteLine(person.Name); // okazuki

            // 生成元のオブジェクトのプロパティが更新されると
            rp.Value = "xin9le";
            // xin9le は Where でフィルタリングされるので Person 側には表示されない
            Console.WriteLine(person.Name); // okazuki のまま
        }
    }

    class Person : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;
        private string _name;
        public string Name
        {
            get => _name;
            set
            {
                _name = value;
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Name)));
            }
        }
    }
}

ToReactivePropertyAsSynchronized はバリデーションにも対応しています。デフォルトではバリデーションエラーがあっても元になったオブジェクトのプロパティに値を書き戻しますが、ignoreValidationErrorValue 引数に true を設定することでバリデーションエラーの値を自動的に元になったオブジェクトに書き戻さなくなります。

以下の例は空文字の場合は、元になったオブジェクトに書き戻さないようにした例です。

using Reactive.Bindings.Extensions;
using System;
using System.ComponentModel;

namespace RxPropLab
{
    class Program
    {
        static void Main(string[] args)
        {
            var person = new Person();

            // 同期するプロパティを指定して ReactiveProperty を生成する
            var rp = person.ToReactivePropertyAsSynchronized(x => x.Name,
                ignoreValidationErrorValue: true)
                .SetValidateNotifyError(x => string.IsNullOrEmpty(x) ? "空" : null);

            // ReactiveProperty を更新すると
            rp.Value = "okazuki";
            // 生成元のオブジェクトのプロパティが更新されます
            Console.WriteLine(person.Name); // okazuki

            // 空文字の場合はバリデーションエラーになるので
            rp.Value = "";
            // Person クラスの値は更新されません
            Console.WriteLine(person.Name); // okazuki のまま
        }
    }

    class Person : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;
        private string _name;
        public string Name
        {
            get => _name;
            set
            {
                _name = value;
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Name)));
            }
        }
    }
}

単一方向同期

INotifyPropertyChanged を実装していないクラスには ReactiveProperty.FromObject(...) メソッドを使って ReactiveProperty から元になったオブジェクトへの書き戻しのみに対応した ReactiveProperty が作成できます。
元になったオブジェクトから ReactiveProperty への値の反映は ReactiveProperty を作成した時に一度だけ行われます。

コード例を以下に示します。

using Reactive.Bindings;
using Reactive.Bindings.Extensions;
using System;
using System.ComponentModel;

namespace RxPropLab
{
    class Program
    {
        static void Main(string[] args)
        {
            var person = new Person();

            // 同期するプロパティを指定して ReactiveProperty を生成する
            var rp = ReactiveProperty.FromObject(person, 
                x => x.Name,
                ignoreValidationErrorValue: true)
                .SetValidateNotifyError(x => string.IsNullOrEmpty(x) ? "空" : null);

            // ReactiveProperty を更新すると
            rp.Value = "okazuki";
            // 生成元のオブジェクトのプロパティが更新されます
            Console.WriteLine(person.Name); // okazuki

            // 空文字の場合はバリデーションエラーになるので
            rp.Value = "";
            // Person クラスの値は更新されません
            Console.WriteLine(person.Name); // okazuki のまま

            // ReactiveProperty.FromObject で作ったのでプロパティを更新しても
            rp.Value = "okazuki";
            person.Name = "xin9le";
            Console.WriteLine(rp.Value); // okazuki のまま
        }
    }

    class Person : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;
        private string _name;
        public string Name
        {
            get => _name;
            set
            {
                _name = value;
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Name)));
            }
        }
    }
}

INotifyPropertyChanged を実装したクラスのプロパティの値を読み取り専用で ReactiveProperty にしたい場合は既に紹介した ObserveProperty メソッドから ToReadOnlyReactiveProperty/ToReadOnlyReactivePropertySlim を呼び出して ReadOnlyReactiveProperty/ReadOnlyReactivePropertySlim に変換してください。この方法は Slim がつく ReactiveProperty も対応しています。

前編まとめ

ということで、前編では ReactiveProperty というライブラリの名前にもなっている ReactiveProperty 系のクラスのよく使うであろう機能を紹介しました。
この後おそらく中編・後編の3本仕立てで残りの ReactiveProperty の機能を解説していきたいと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ASP.NET Core 3.1のIdentityで認証の確認メールをSMTPで行う

初めに

 「ASP.NET Core 3.1でPostgreSQLを利用してIdentityで認証を使えるようにする」を記述している中で、除外したユーザー登録のSMTPでの確認メールを試してみました。とりあえず送信できるところまでで、送信時のエラー等の対処は行っていません。このあたりの対処の問題で、マイクロソフトはSMTPの利用を推奨していない様です。

環境

 今回の環境は以下の通りです。
- VisualStudio2019 Ver.16.6.4
- ASP.NET Core 3.1
- MailKit 2.8.0
- 認証を組み込んだテンプレートでプロジェクトが作成されていること
- データベースのマイグレーションが行われて利用可能になっていること

実装

テンプレートの確認

 テンプレートで認証有のプロジェクトを作成すると、「Startup.cs」の「ConfigureServices()」で基本的な認証が以下のように設定されます。

Startup.cs
...
public void ConfigureServices(IServiceCollection services)
{
    ...
    services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
    ...
}

 この時に設定されているオプションの「RequireConfirmedAccount」がアカウントの確認を要求するオプションで、メールを利用してアカウントの確認を行うことを要求しています。これが設定されていると、アカウントの確認ができていない利用者ではログインできません。確認が必要ない場合はこのオプションを消してしまえば良いのですが、確認を実行したい場合はメール送信の設定が必要なので、「IEmailSender」インタフェースを実装したクラスを作成してサービス登録する必要があります。

SMTPパッケージ

 送信でSMTPを利用するために、SMTPのパッケージを利用します。今回はNuGetで「MailKit」パッケージを追加します。

メール送信パラメータ

 メール送信用の送信元の情報ですが、「appsettings.json」から取得しようと思います。そのため、DIで利用できるようにメール送信パラメータのクラスを作成します。

SendMailParams.cs
namespace WebApplication1.MailSender
{
    /// <summary>
    /// メール送信のパラメータ
    /// 「appsettings.json」から受け取るために利用
    /// </summary>
    public class SendMailParams
    {
        public string MailServer { get; set; }
        public int Port { get; set; }
        public string User { get; set; }
        public string Password { get; set; }
        public string SendAddress { get; set; }
    }
}

 「Startup.cs」の「ConfigureServices」メソッドで以下のようにすることでDIの対象となります。

Startup.cs
...
public void ConfigureServices(IServiceCollection services)
{
    ...
    services.Configure<SendMailParams>(Configuration.GetSection("SendMailParams"));
    ...
}

実際のパラメータは「appsettings.json」で以下のように設定しています。

appsettings.json
{
  ...

  "SendMailParams": {
    "MailServer": "<メールサーバ>",
    "Port": 587,
    "User": "<ログインユーザーID>",
    "Password": "<ログインパスワード>",
    "SendAddress": "<送信元メールアドレス>"
  }
}

メール送信クラス

 認証で利用するメール送信クラスは以下の通りです。エラーの対処などは行っていません。

MailSender.cs
using MailKit.Net.Smtp;
using MailKit.Security;
using Microsoft.AspNetCore.Identity.UI.Services;
using Microsoft.Extensions.Options;
using MimeKit;
using System.Threading.Tasks;

namespace WebApplication1.MailSender
{
    public class MailSender : IEmailSender
    {
        /// <summary>
        /// メール送信のパラメータ
        /// </summary>
        SendMailParams _sendMailParams;

        /// <summary>
        /// メール送信のパラメータをDIで受け取るコンストラクタ
        /// </summary>
        /// <param name="optionsAccessor"></param>
        public MailSender(IOptions<SendMailParams> optionsAccessor)
        {
            _sendMailParams = optionsAccessor.Value;
        }

        /// <summary>
        /// IEmailSenderのメール送信の実装メソッド
        /// </summary>
        /// <param name="email">送信先メールアドレス</param>
        /// <param name="subject">送信メールタイトル</param>
        /// <param name="message">送信メールメッセージ</param>
        /// <returns>メール送信タスク</returns>
        public Task SendEmailAsync(string email, string subject, string message)
        {
            return Execute(subject, message, email);
        }

        /// <summary>
        /// メール送信の実際の動作タスク
        /// </summary>
        /// <param name="subject">送信メールタイトル</param>
        /// <param name="message">送信メールメッセージ</param>
        /// <param name="email">送信先メールアドレス</param>
        /// <returns></returns>
        public async Task Execute(string subject, string message, string email)
        {
            var emailMessage = new MimeMessage();

            emailMessage.From.Add(new MailboxAddress(_sendMailParams.User, _sendMailParams.SendAddress));

            emailMessage.To.Add(new MailboxAddress(email, email));

            emailMessage.Subject = subject;

            emailMessage.Body = new TextPart("plain") { Text = message };

            using (var client = new SmtpClient())
            {
                await client.ConnectAsync (_sendMailParams.MailServer, _sendMailParams.Port, SecureSocketOptions.Auto);
                await client.AuthenticateAsync(_sendMailParams.User, _sendMailParams.Password);
                await client.SendAsync(emailMessage);
                await client.DisconnectAsync(true);
            }
        }
    }
}

 これを利用するために最終的に「Startup.cs」の「ConfigureServices」は以下のようになります。

Startup.cs
...
public void ConfigureServices(IServiceCollection services)
        {
            services.AddDbContext<ApplicationDbContext>(options =>
                options.UseNpgsql(
                    Configuration.GetConnectionString("DefaultConnection")));

            // メール送信用のパラメータを「appsettings.json」から抜きしてDIで利用できるようにしている
            services.Configure<SendMailParams>(Configuration.GetSection("SendMailParams"));

            // メール送信用のクラスを認証時に利用するメール送信サービスとして登録。これで認証時に確認メールが送信できるようになる
            services.AddScoped<IEmailSender, MailSender.MailSender>();

            // メールによる確認を有効にした認証を有効にする
            services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
                .AddEntityFrameworkStores<ApplicationDbContext>();
            services.AddRazorPages();
        }

テスト

 これでメール送信の設定ができたので、起動して最初の画面の右上の「Regist」でユーザー登録します。もちろん、送信メールサーバーなどの設定や、ネットワークへの接続はできる状態にしておいてください。ユーザー登録を実施すると、登録したメールアドレス宛てに、以下のような確認メールが送信されます。ここで、リンクをクリックして登録を実施すると確認完了画面が表示され、このメールアドレスでログインできるようになります。

確認メール
Please confirm your account by <a href='https://localhost:44359/Identity/Account/ConfirmEmail?userId=...' >clicking here</a>.

confirmRegist.png

 メール内容が英語なので、実際に使うには認証をスキャフォールディングして該当ソースを探して変更する必要がありそうです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ASP.NET Core 3.1でPostgreSQLを利用してIdentityで認証を使えるようにする

初めに

 以前、同様の内容をASP.Net Core 2.2の環境で書いてみたのですが、最近3.1の環境に移行するにあたって、気づいた点を書いておきます。元ネタはASp.Net Core でPostgreSQLを利用してIdentityで認証を使えるようにする(1)にありますので、参照してください。

環境

 今回の環境は以下の通りです。
- VisualStudio2019 Ver.16.6.4
- ASP.NET Core 3.1
- PostgreSQL 12 インストール済み、接続用のアカウント作成済み

プロジェクトの作成からデータベースの設定まで。

プロジェクト作成

 ASP.NET Core 2.2の時と同じです。作成時にASP.NET Core 3.1を選んでください。

Postgres用EntityFrameworkの設定

 「appsettings.json」はASP.NET Core 2.2の時と同じように変更します。
 「Startup.cs」のメソッド「ConfigureServices」は内容が変更されていますが、変更箇所は同じで、「options.UseSqlServer」の部分を「options.UseNpgsql」に変更します。「services.AddMvc()」が「services.AddRazorPages()」とレーザーページ専用のメソッドができているのが面白いです。

PostgreSQLが古いとエラーになる

 Postgresqlのバージョンには注意が必要です。以前の記事の時のバージョン9.6で「Update-Database」を実行すると、エラーになります( SqlState: 42601、MessageText: "GENERATED"またはその近辺で構文エラー)。どうやらSQLコマンドに変更があり新しいバージョンでないと対応できない様です。ネットで調べると「SetCompatibilityVersion」メソッドで対応できそうな記事がありましたが、ダメでした。作成されているDB構築のSQLを修正すればいけるでしょうが、後々DBの変更で大変なのでしません。
 また、試しに「Npgsql.EntityFrameworkCorePostgreSQL」を当時のバージョンに落としてみると、今度は「Add-Migration」で失敗しました。
 結論としてはPostgreSQLは新しいもの(おそらく10以上と思うけど試してません。今回は12を使いました)を使ってください。

でもこのまま「Regist」でユーザーを作っても「Login」でログインできない

 この状態で実行して画面右上の「Regist」で表示された画面でユーザーを登録し、「Login」でログインしようとすると失敗します。
 基本的にこの認証をそのまま使う気はないので対応する必要もないのですが、気になるので調べてみました。「Startup.cs」の「ConfigureServices()」で「services.AddDefaultIdentity()」の処理にオプション「options => options.SignIn.RequireConfirmedAccount = true」が設定されていますが、これを消してしまえばいいです。
 このオプションはメールによってIDの確認を行うようで、メール送信の設定をしていないと確認が失敗してログインできません。
 マイクロソフトの「ASP.NET Core でのアカウントの確認とパスワードの回復」には「SendGrid」というメールサービスを利用する方法が書かれており、「SMTP」を利用する方法は推奨しない旨の記述があります。ASP.NET CoreにあったSMTPも使えなくなっているようです(確認してませんが)。
 それでもSMTPを使いたい人向けに別途記事を「ASP.NET Core 3.1のIdentityで認証の確認メールをSMTPで行う」に書いておきます。

ロールの利用と認証必須の設定

 ロールの追加はASP.NET Core 2.2の時と同じように「services.AddDefaultIdentity()」に対して「AddRoles()」で行います。
 承認が未指定のページを全てデフォルトで承認が必要にするためのオプションは、ASP.NET Core 2.2では「services.AddMvc()」の中にオプションとして組み込んでいたのですが、ASP.NET Core 3.1では追加された「services.AddAuthorization」メソッドとそのオプションで行うようです。

Startup.cs
public void ConfigureServices(IServiceCollection services)
        {
            services.AddDbContext<ApplicationDbContext>(options =>
                options.UseNpgsql(
                    Configuration.GetConnectionString("DefaultConnection")));

            // 認証を有効にする
            //services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
            services.AddDefaultIdentity<IdentityUser>()
                .AddRoles<IdentityRole>() // <<== ロールを利用するためにの追加
                .AddEntityFrameworkStores<ApplicationDbContext>();
            services.AddRazorPages();

            // 基本的に指定のないページはすべて認証が必要になるように設定
            services.AddAuthorization(options =>
            {
                options.FallbackPolicy = new AuthorizationPolicyBuilder()
                    .RequireAuthenticatedUser()
                    .Build();
            });
        }

 これを設定して実行すると、最初に「WELCOME」のページが表示されず、ログイン画面が表示されるようになります。ログインできると「Privacy」画面が表示され、「Home」をクリックすると「WELCOME」のページが表示されます。

初期のロールとシステム管理ユーザーの作成

 システムの初期設定として、管理ユーザーと一般ユーザー、システム管理のロールを作成するクラスを以下のように作りました。(ASP.NET Core 2.2と少し変わってます。)

IdentityUserInitializer.cs
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Threading.Tasks;

namespace WebApplication1.Areas.Identity
{
    public class IdentityUserInitializer
    {
        // 初期化時のロール
        public static readonly string SystemManagerRole = "SystemManager";      // システム管理権限

        // 初期化時のシステム管理ユーザーID
        public static readonly string StstemManageEmail = "system@test.com";            // 最初のシステム管理ユーザーのメールアドレス
        public static readonly string StstemManagePassword = "!initialPassword01";      // 最初のシステム管理ユーザーの初期パスワード
        public static readonly string NormalUserEmail = "user@test.com";                // テスト用一般ユーザーのメールアドレス
        public static readonly string NormalUserPassword = "!User01";                   // テスト用一般ユーザーの初期パスワード

        /// <summary>
        /// ユーザーとロールの初期化
        ///  初期のシステムユーザーあが存在しない場合のみ内容が実行される。存在する場合は何もせずに終了
        /// </summary>
        /// <param name="serviceProvider"></param>
        public static async Task Initialize(IServiceProvider serviceProvider)
        {
            // ユーザー管理を取得(using Microsoft.Extensions.DependencyInjectionがないとエラーになる)
            var userManager = serviceProvider.GetService<UserManager<IdentityUser>>();

            // 初期のユーザーマネージャーが存在しなければロールの作成と初期システムユーザーを作成する
            var systemManager = await userManager.FindByNameAsync(StstemManageEmail);
            if (systemManager == null)
            {
                // ロール管理を取得
                var roleManager = serviceProvider.GetService<RoleManager<IdentityRole>>();

                // ロールの追加
                await roleManager.CreateAsync(new IdentityRole(SystemManagerRole));    // システム管理ロール

                // 初期システム管理者の作成
                // なぜか知らないが、デフォルトのログイン画面はユーザーIDではなくメールアドレスを要求し、バリデーションもメールで設定されている。
                // ところが、ログイン処理自体は「Email」ではなく「UserName」で行われるので両方に設定せざるを得ない。
                // なんでこんなことのなっているのか? 変更するにはログイン画面を変えればいい
                systemManager = new IdentityUser { UserName = StstemManageEmail, Email = StstemManageEmail, EmailConfirmed = true };
                await userManager.CreateAsync(systemManager, StstemManagePassword);

                // システム管理ユーザーにシステム管理ロールを追加
                systemManager = await userManager.FindByNameAsync(StstemManageEmail);
                await userManager.AddToRoleAsync(systemManager, SystemManagerRole);

                // テスト用の一般ユーザー作成
                var normalUser = new IdentityUser { UserName = NormalUserEmail, Email = NormalUserEmail, EmailConfirmed = true };
                await userManager.CreateAsync(normalUser, NormalUserPassword);
            }
        }
    }
}

 これを実行するために「Program.cs」の「Main()」を以下のように変更します。

Program.cs
        public static void Main(string[] args)
        {
            // これが元の1行
            //CreateHostBuilder(args).Build().Run();

            var host = CreateHostBuilder(args).Build();

            using (var scope = host.Services.CreateScope())
            {
                // サービスプロバイダーの取得
                var services = scope.ServiceProvider;

                // 初期のユーザーとロールの作成
                IdentityUserInitializer.Initialize(services).Wait();
            }

            host.Run();
        }

ロールの変更とロール動作の確認

 ここで一度実行して、ユーザー「user@test.com」/パスワード「!User01」でログインして「Privacy」が表示されることを確認します。
 次に、テンプレートで作成されている「Page/Privacy.cshtml.cs」ファイルを開き、以下のようにクラスにロールアトリビュートを設定します。

Privacy.cshtml.cs
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Logging;

namespace WebApplication1.Pages
{
    [Authorize(Roles = "SystemManager")] <= ロールを追加
    public class PrivacyModel : PageModel
    {
        private readonly ILogger<PrivacyModel> _logger;

        public PrivacyModel(ILogger<PrivacyModel> logger)
        {
            _logger = logger;
        }

        public void OnGet()
        {
        }
    }
}

 再度実行して「user@test.com」でログインし、「Privacy」を開いてみると、拒否されます。ユーザー「system@test.com」/パスワード「!initialPassword01」でログインすると「Privacy」が表示されます。

自動マイグレーション

 システムが変更されてDBが変更された場合に、常に手動で「Update-Database」を実行するのは、リリース時に作業忘れを発生しそうなので、自動マイグレーションを設定します。常に手動でしたい場合は、ここは無視してください。(自動マイグレーションでも「Add-Migration <キー>」はする必要がありますが、これは開発環境だけです。自動マイグレーションを設定しておくと、実環境にリリースした際にその環境で実施する「Update-Database」を省けます)

 「Program.cs」を以下の様に変更します。

Program.cs
using Microsoft.AspNetCore.Hosting;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using WebApplication1.Areas.Identity;
using WebApplication1.Data;

namespace WebApplication1
{
    public class Program
    {
        public static void Main(string[] args)
        {
            // これが元の1行
            //CreateHostBuilder(args).Build().Run();

            var host = CreateHostBuilder(args).Build();

            using (var scope = host.Services.CreateScope())
            {
                // サービスプロバイダーの取得
                var services = scope.ServiceProvider;

                // データベースの自動マイグレーション
                var context = services.GetRequiredService<ApplicationDbContext>();
                context.Database.Migrate();

                // 初期のユーザーとロールの作成
                IdentityUserInitializer.Initialize(services).Wait();
            }

            host.Run();
        }

        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureWebHostDefaults(webBuilder =>
                {
                    webBuilder.UseStartup<Startup>();
                });
    }
}

 実施後、データベースをいったん削除して自動的にDBが作成されることを確認してください。

デフォルトのページを利用しないように設定する

 ASP.NET Core 2.2の時と同じように「AddDefaultIdentity」をやめるのですが、3.0からマイクロソフトのページに「完全な Identity UI ソースの作成」というタイトルで記述されていました(AddMVC()のパターンでしたが)。
 それをもとにして変更したのが以下の部分です。

Startup.cs
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddDbContext<ApplicationDbContext>(options =>
                options.UseNpgsql(
                    Configuration.GetConnectionString("DefaultConnection")));

            //// メール送信用のパラメータを「appsettings.json」から抜きしてDIで利用できるようにしている
            //services.Configure<SendMailParams>(Configuration.GetSection("SendMailParams"));

            //// メール送信用のクラスを認証時に利用するメール送信サービスとして登録。これで認証時に確認メールが送信できるようになる
            //services.AddScoped<IEmailSender, MailSender.MailSender>();

            // 認証を有効にする
            //services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
            //services.AddDefaultIdentity<IdentityUser>()
            services.AddIdentity<IdentityUser, IdentityRole>()
                //.AddRoles<IdentityRole>() // <<== ロールを利用するためにの追加 <= AddIdentityでは不要みたい
                .AddEntityFrameworkStores<ApplicationDbContext>()
                .AddDefaultTokenProviders();

            // AddMvc()の場合はオプションを設定する必要がありそうだが...
            services.AddRazorPages();

            // これを追加する
            services.ConfigureApplicationCookie(options =>
            {
                options.LoginPath = $"/Identity/Account/Login";
                options.LogoutPath = $"/Identity/Account/Logout";
                options.AccessDeniedPath = $"/Identity/Account/AccessDenied";
            });

            // 基本的に指定のないページはすべて認証が必要になるように設定
            services.AddAuthorization(options =>
            {
                options.FallbackPolicy = new AuthorizationPolicyBuilder()
                    .RequireAuthenticatedUser()
                    .Build();
            });
        }

ここで、ログインとログアウトとアクセス拒否のページは必要なので、スキャフォールディング(「ASp.Net Core でPostgreSQLを利用してIdentityで認証を使えるようにする(3)」の「Identityをスキャフォールディングしてみる」参照)で、そのページのみ(「Account¥AccessDenied」「Account¥Login」「Account¥Logout」)取り込む。

 これで実行すると、同じようにログイン、ログアウト、アクセス拒否は表示されますが、それ以外の「Regist」は実行されなくなります。URLを直接入力しても、存在しない旨の表示がされます。

 以上、3.1でのidentityのカスタマイズでした。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

.NetCore3.1+Next.js+TypeScriptの環境構築

構成

業務で下記構成でWebアプリケーションを開発中です。初期構築したときの手順をちょっとまとめさせてください。

  • サーバ側
    • WindowsSever2019
    • .NetCore3.1
    • IIS
  • フロント側
    • node.js 12.8.0
    • Typescript 3.9.7
    • next.js 9.4.4

サーバ側

「DotNetTest」という名前の.NetCore3.1のAPIプロジェクトを作成します。

dotnet new webapi --framework netcoreapp3.1 --output DotNetTest
DotNetTest
│  appsettings.Development.json
│  appsettings.json
│  DotNetTest.csproj
│  Program.cs
│  Startup.cs
│  WeatherForecast.cs
│
├─Controllers
│
├─Properties
│      launchSettings.json
│
├─Content
│  └─frontend
│
└─wwwroot

静的ファイルを提供するミドルウェアを登録します。

Startup.cs
// Configureメソッド内
app.UseDefaultFiles();
app.UseStaticFiles();

Content/frontendにNext.jsのソースを格納し、wwwrootにコンパイル後のコードを格納する形にしたいと思います。

フロント側

作ります。

mkdir Content
cd Content
npx create-next-app frontend
cd frontend
npm install --save-dev typescript @types/react @types/node
touch tsconfig.json
npm run dev

package.jsonを書き換えます。scriptsに下記文言を追加しました。

package.json
"build": "next build && next export -o ../../wwwroot",

npm run buildして終わり、簡単でございました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[ASP.Net Core] How to manage access to the file (Azure Blob) using ASP.Net Identity Core

Here is how to mange access to the file (which is stored on Azure Blob) using ASP.Net Identity .

For more details about ASP.Net Identity, please refer to this document
https://docs.microsoft.com/en-us/aspnet/identity/

Azure Blob has several ways to control access to the file, as below.

  • Shared Key (storage account key)
  • Shared access signature (SAS)
  • Azure Active Directory (Azure AD)

Please read here for details.
https://docs.microsoft.com/en-us/azure/storage/common/storage-auth

If you are using APS.Net Identity, the simple way to manage access to the file is generating Shared access signature along with making request to the file.

For making Shared access signature, please refer to this document.
https://docs.microsoft.com/en-us/azure/storage/common/storage-sas-overview

Withing Share access signature (SAS), there are there types.

User delegation SAS

A user delegation SAS is secured with Azure Active Directory (Azure AD) credentials and also by the permissions specified for the SAS.

For this we are not using Azure AD, so we ll skip this type.

Account SAS

An account SAS is secured with the storage account key. An account SAS delegates access to resources in one or more of the storage services. All of the operations available via a service or user delegation SAS are also available via an account SAS. Additionally, with the account SAS, you can delegate access to operations that apply at the level of the service, such as Get/Set Service Properties and Get Service Stats operations. You can also delegate access to read, write, and delete operations on blob containers, tables, queues, and file shares that are not permitted with a service SAS.

We are focused on reading files on Azure Blob, so we ll skip this type.

Service SAS

A service SAS is secured with the storage account key. A service SAS delegates access to a resource in only one of the Azure Storage services: Blob storage, Queue storage, Table storage, or Azure Files.

For generating service SAS, please refer here.
https://docs.microsoft.com/en-us/rest/api/storageservices/create-service-sas

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む