ぷろじぇくと、みすじら。

C# で null 許容型あれこれ

Created at:

.NET Core 3.0 のリリースとともに C# 8.0 がきて、null 許容型 (nullable reference types) を使えるようになったので使ってみたメモです。

未初期化だけど後で初期化するパターン

コンストラクターではまだ初期化しないのだけど、いずれフレームワークから初期化するので API を利用する側はほぼ null を扱わない、けど内部の初期状態は null なのを許してほしいというケースはまあよくあります。Kotlin の lateinitTypeScript の definite assignment assertion みたいなやつですね。

C# ではズバリそれっぽいものはないので default! を初期値として代入しておけばよいです。

lateinit とかもそうですが適当に使うと治安が最高に悪くなるので気を付けたほうがいいやつです。

ジェネリクス

class 制約のないジェネリクスの場合 ? をつけて null 許容型であると宣言できません。そのため null を扱うかもしれない、というケースを宣言するには属性を使用することになります。

逆を言えば where T: classwhere T: unmanaged のような制約をつけておけば T? と null 許容型を素直に書くことができるので書けるときは書いてしまうのが手です。

AllowNull 属性と DisallowNull 属性 (事前条件)

例えば次のようなメソッドの引数で参照型のときは null 渡したいケースがあります。

そのままでは非許容なので null が来てもよいということをコンパイラーに伝える必要があるので、 AllowNull 属性を使用します。

逆に null が入ってきてほしくないというのを明示することもできます。例えば先ほどのコードをもう一度。

このような場合は DisallowNull 属性を使用すると null を許さないということを明示できます。

DisallowNull 属性は .NET Core のコードであれば IEqualityComparer<T>.GetHashCode などに見られます。

MaybeNull 属性と NotNull 属性 (事後条件)

次は値型ならデフォルト値、参照型なら null が返るようなメソッドを扱うケースです。GetValueOrDefaultDefaultIfEmpty のようなものでよくありますね。

このようなケースをカバーするには MaybeNull 属性を使うと戻り値が null になる可能性があることを表せます。なお、戻り値に対しての属性なので return: です。

逆に絶対 null を返さないということがわかっている場合には NotNull 属性で宣言できます。例えば次のようなケースです。

プロパティ

AllowNull 属性と DisallowNull 属性 (事前条件)

プロパティの setter で null を許可したかったり拒否したかったりというケースがあるかもしれません。例えば setter で null はセットできるけれども null は返ってこないような API です(というのはあまり想像しづらいですが…)。

ちなみにコンパイラーが中途半端に賢いので Value = null; の後に Value を使用しようとすると怒られます(が、実際は null じゃないはずなのでなんか変な気がします)。

逆は例によって DisallowNull です。null が返るかもしれないけれど null をセットすることは許さないようなケースです。Microsoft のドキュメントのサンプルではこんな感じです

初期化時は null が返るけど、ユーザーがセットすることは許さないみたいな感じですかね。

その他のケース

NotNullWhen 属性 と MaybeNullWhen 属性 (事後条件)

TryGetValueTryParse のような、成功時には out 引数で値を返すがそれ以外では null を返し、成否は戻り値にするというケースがあります。例えば次のようなコードがあるとします。

この MyDictionary を使う場合は次のようなコードになるわけですが、その際 null の扱いも上手く処理されてほしいわけです。

そこで戻り値によって nullable かどうかを伝える NotNullWhen という属性が用意されています。この属性を付けると「nullable な型が場合によってはその後 non-nullable 確定できるかも」という情報をコンパイラーに伝えられます。

他の使い道としては String.IsNullOrEmpty みたいなもので使えます(使われています)。 IsNullOrEmpty も後のフロー解析に影響を与えてほしい側面を持っているのでピッタリですね。

逆の意味を持つのが MaybeNullWhen 属性で non-nullable につけた MaybeNullWhen(false)NotNullWhen(true) と同じになります。

これらの属性の使い分けですが

例えば TryGetValue のようなもので MaybeNullWhen 属性を使った場合、前提として non-nullable になると out var で宣言したローカル変数が non-nullable になり、if のようなフロー解析から外れたときに non-nullable (ただし null が入っている可能性がある) という状況になります。

同様に IsNullOrEmpty のようなもので MaybeNullWhen 属性を使った場合、引数は前提として non-nullable なので String? な値を引数に渡すたびに ! を付けてあげないといけないことになります。

…と、ここまで読むと MaybeNullWhen 属性を使わなくてもほとんどのケースでは NotNullWhen 属性で事足りるのではと思うのですが、制約なしジェネリクスの型パラメータに ? を付けて null 許容型とすることはできないのでその場合には MaybeNullWhen を使う必要があります。

NotNullIfNotNull 属性 (事後条件)

入力が null ではなかったら絶対に null ではないということがわかっていて、コンパイラーに伝えたいというケースに使えるのが NotNullIfNotNull 属性です。何を言っているのかみたいな名前…。

具体的なケースであれば例えばエスケープ処理のようなもので、文字列を受け取るけれども null が来たら null を返すが、それ以外は絶対値が返るようなメソッドです(その仕様がいいかどうかはさておき)。

まとめ

属性を書き始めると書き味が悪くなるのと IntelliSense などでシグネチャヒントを見ただけではわからないという問題が起きやすくなるので、新規に書き起こすものは極力属性を書かなくて済むような API にするほうが望ましいように感じました。

例えば NotNullIfNotNull 属性のようなものなどはそもそも引数で null を受けない、TryGetValue などは戻り値と out を使わないで null 許容型をそのまま返すといった形にできます。

属性は既存のコードを壊さず null 許容型との相互運用のためにアノテーションをつけていくものというぐらいの認識がいいかもしれません。まあジェネリクスが厳しいのですが…。

Google (GSuite) を IdP として Azure Active Directory (Office 365) にサインインする

Created at:

Azure Active Directory(以下Azure AD)とGoogleのアカウント連携について調べるとAzure AD (Office 365)をIdPつまりユーザー情報のソースとしてGoogleにサインイン (SSO) する、ユーザープロビジョニングを行うといった設定についてのドキュメントが見つかります。

一方で逆のパターン、つまりGoogle (GSuite, Cloud Identity)をIdPとしてAzure ADやOffice 365にサインインしたり、ユーザー情報を同期するパターンについての説明は少ないので折角ですしメモもかねて残しておきます。

ちなみにAzure AD B2Bの機能としてGoogleをアプリとして登録することでAzure ADにゲストとして登録する機能というのもあるのですがそれとは別です。

公式ドキュメント(Office 365 クラウド アプリケーション - G Suite 管理者 ヘルプ)ushiyasanさんのブログエントリー(GoogleアカウントでOffice365(Azure AD)にSSOログイン)を参考にして大体設定しています。というわけで基本は公式のドキュメントの手順に沿って設定していきます。

Google の IdP 設定を取得する

公式ドキュメントの手順1で、ここはドキュメントそのままです。まずは Google の管理コンソールから設定のための情報を取得します。

特権管理者で管理コンソールにログインして セキュリティシングルサインオン (SSO) の設定 を開いて、各種情報のメモと証明書のダウンロードを…と思ったのですが実はSAMLアプリケーション (Office 365)の追加の手順の途中でも表示されるのでそっちからでもよいです。

特権管理者で アプリSAML アプリサービスやアプリをドメインに追加 をクリックして、SAMLアプリを追加する画面を表示します。SAML アプリケーションで SSO を有効にする という画面が出るのでOffice 365 で検索します。Microsoft Office 365 というアプリが見つかるのでそれをクリックして進めます。

ステップ2 Google IdP 情報 という画面が表示されるとGoogleをIdPとして利用するために必要な情報が表示されますので下記の3つの情報を記録しておきます。

Azure AD をフェデレーテッドモードに変更する

公式ドキュメントの手順 2で、ここからはAzure AD側の設定を行います。先のブログエントリーでも書かれているのですが、手順 2についての手順はPowerShellで設定してぐらいのざっくりとしたことしか書かれていません。

やることは次の二点です。

PowerShell モジュールをインストールし、接続する

Azure ADの設定にはPowerShellで接続できる必要があるのでその準備です。

まずはモジュールをインポートします。

Import-Module -Name MSOnline

次にサービスに接続します。管理権限のあるユーザーで接続してください。

PS> Connect-MsolService
PS> Get-MsolDomain
Name                         Status   Authentication
----                         ------   --------------
example.com                  Verified Managed
examplecom.onmicrosoft.com   Verified Managed

既存ユーザーの ImmutableId を設定する

GoogleはAzure AD側のユーザーと突き合わせるためにAzure ADのユーザープロパティ ImmutableId を使用します。

ImmutableId に使用する値は特に理由がなければメールアドレス (例: `user@example.com`) にします。他でもできるはずですが、Google側が受け入れるのがメールアドレスか姓名ぐらいしかないようです。

Azure AD単体で使っている状態ではユーザーに対して設定されていないので既存のユーザーに関しては何らかの方法で設定してあげます。まあ何らかの方法というか Set-MsolUser コマンドレットですね。

Set-MsolUser -UserPrincipalName alice@example.com -ImmutableId alice@example.com
Get-MsolUser | ?{ $_.UserPrincipalName.EndsWith("example.com") } | %{ Set-MsolUser -UserPrincipalName $_.UserPrincipalName -ImmutableId $_.UserPrincipalName }

後々ユーザープロビジョニングでGoogle側からやってきたユーザーに関しては自動で設定されます。

Azure ADの認証モードを Federated に変更し、SAML認証の設定をする

Azure ADの認証モードを Federated に変更し、SAML関連の設定をおこなうことで認証をAzure AD以外の場所で行うようにします。

設定には Set-MsolDomainAuthentication コマンドレットを使用して、先ほどのIdP情報を指定します。

# "SSO の URL" と書かれていた項目
$ssoUrl = "https://accounts.google.com/o/saml2/idp?idpid=<IdPId>"

# "エンティティ ID" と書かれていた項目
$entity = "https://accounts.google.com/o/saml2?idpid=<IdPId>"

# 対象のドメイン名
$domain = "example.com"

# 証明書
$cert = "ダウンロードした証明書の -----BEGIN CERTIFICATE----- から -----END CERTIFICATE----- までの「間」を改行をなしで一行で"

Set-MsolDomainAuthentication -Authentication Federated  -DomainName $domain -ActiveLogOnUri $ssoUrl -PassiveLogOnUri $ssoUrl -IssuerUri $entity -LogOffUri $ssoUrl -SigningCertificate $cert -PreferredAuthenticationProtocol SAMLP
PS> Get-MsolDomain
Name                         Status    Authentication
----                         ------    --------------
example.com                  Federated Managed
examplecom.onmicrosoft.com   Verified  Managed

下記のようなエラーが発生した場合には指定したドメインがAzure ADのプライマリドメインとなっていると思うので onmicrosoft.com や他のドメインにプライマリを一度切り替える必要があります。

Set-MsolDomainAuthentication : You cannot remove this domain as the default domain without replacing it with another
default domain. Use the the Set-MsolDomain cmdlet to set another domain as the default domain before you delete this
domain.

Google (GSuite) の SAML アプリケーション設定

再びGoogleの管理コンソールに戻ってステップを進めます。

Microsoft Office 365 の基本情報 はそのままで「次へ」で進めます。

サービス プロバイダの詳細 は「署名付き応答」にチェックを入れて「次へ」で進めます。

属性のマッピング は「IDPEmail」を「基本情報」「メインのメールアドレス」を選択して「次へ」で進めます。

これでアプリの基本設定は完了ですがまだ有効になっていないので動きません。

Microsoft Office 365 アプリを有効化する

SAMLアプリを追加しただけでは有効になっていないのでそれを有効化する必要があります。

アプリSAML アプリ でアプリの一覧から Microsoft Office 365 を選択して、右上の「サービスを編集」をクリックします。

「サービスのステータス」で「オン (すべてのユーザー)」を選択します。

サインインをしてみる

ここまでの設定でAzure ADにGoogle経由でサインインできるようになっているはずでしょう。多分。

Cookieが残っていると挙動が怪しいのでプライベート ブラウジングやIn Private ウィンドウなどでテストするのをオススメします。

トラブルシューティング: Google側で 400 エラーが発生する

  1. That’s an error.
    Error parsing the request, No SAML message present in request That’s all we know.

Azure ADから一度Googleにリダイレクト後、上記のようなエラーメッセージが出て進まない場合には Set-MsolDomainAuthentication コマンドレットで設定時に -PreferredAuthenticationProtocol SAMLP が指定されてなく、SAMLではなくWSで認証をかけている可能性があります。

トラブルシューティング: 無限サインインループ

無限サインインループになった場合はAzure AD側に ImmutableId が設定されていない可能性や設定が間違っている可能性があるのでそちらを確認してください。

ユーザー プロビジョニングを設定する

サインインできるようになった後はGoogle側からAzure AD側へユーザー情報を同期するためにユーザープロビジョニングの設定を行います。ユーザープロビジョニングによってGoogle側にユーザーを追加するとAzure ADにユーザーを自動で作成するといったことが可能になります。

ユーザープロビジョニングの設定は アプリSAML アプリ でアプリの一覧から Microsoft Office 365 を選択し、ユーザー プロビジョニング を開きます。

「ユーザー プロビジョニングを設定」をクリックするとダイアログが開くので「承認」をクリックします。

Azure AD側でアクセス許可の確認が表示されるので「組織の代理として同意する」にチェックして「承諾」でGoogleの管理コンソールに戻ります。

最後に属性のマッピング設定画面が表示されるので onPremisesImmutableId基本情報 > ユーザー名 にマッピング設定して「次へ」で完了です。

後はしばらく待つとGoogle側のユーザー情報がAzure AD側へと反映され、ユーザーが作成されたりします。

おわりに

というわけでGoogle (GSuite)をユーザー情報のソースとしてAzure ADを利用できるようになることでOffice 365やAzureに関連したサービス(AzureやAzure DevOps等)などのサインインを一元化できるのでよいのではないでしょうか。

すでにGSuiteを使用している環境ではAzure ADをIdPにするのが怖かったり抵抗がある、めんどくさいということもあるのでこの構成もオススメです。