KNOWLEDGE - COLUMN ナレッジ - コラム

【エバンジェリスト・ボイス】Blazorと、Microsoft Graphの巻

先端技術部
エバンジェリスト ベロフ・ドミトリー (ディーマ)

Microsoft Graphは平たく言えばMicrosoft 365をはじめMicrosoftサービスとそのサービスに格納された情報への窓口です。GraphでMicrosoftサービスにある情報を統一的な方法で取得でき、個人のユーザーにも組織のユーザーにも便利な機能を提供できます。

https://docs.microsoft.com/en-us/graph/overviewからのイメージ

※外部サイト:Overview of Microsoft Graphからのイメージ

ということで、BlazorアプリでGraphからユーザーの情報を取得してみましょう。Graphの機能を前回に作成したアプリに追加しますので、未だであれば「Blazorと、JavaScript召喚の巻」でも読んでみてください。
さてと、どんな情報を取得しようかな? 私、スタバが好きで良く抹茶ラテを買います。購入時にスタバのカードを使っていて、残高がなくなったら自動的に入金されます。入金されると、その旨と入金の金額がメールで来ます。そのメールの情報が使えると思います。まぁ、取り敢えずBlazorアプリで三ヶ月分のメールを表示しましょう^_^

Blazorアプリの拡張の概要

Blazorアプリでメールを表示できる機能を下記のように実装したいと思います。

Blazorアプリでメールを表示できる機能

Graph: GraphのAPI
GraphService: Graphから情報を取得するサービス
Azure AD: ユーザーのログインに使用します
MSAL: ブラウザーでログイン画面を表示し、Azure ADでユーザーをログインさせるライブラリー
AuthService: MSALを.NET側で呼び出せる為のサービス
MSALAuthProvider: GraphServiceがGraphのAPIへアクセスする際、ログインされたユーザーのアクセストークンを設定するサービス
StarbucksMailsページ: メールを表示するページ

Azureのアプリの作成

GraphのAPIにアクセスするには、AzureのアプリのIDが必要なため、まずはそれを作成します。
※会社のアカウントでも個人のアカウントでも使えます。個人のアカウントだと、誰でも気楽に使えるのでここで個人のアカウントを使用します

  1. ※外部サイト:Microsoft Azureに自分のMicrosoftアカウントでログインします
  2. Azure Active Directory ページを開きます。 (何処から開くか、例えば、ページの上の検索に
    Azure Active Directoryを記入します。)
  3. Blazorアプリでメールを表示できる機能

  4. 左のメニューでアプリの登録(App Registration)を選択し、新規登録(New Registration)をクリック。
  5. アプリの登録画面アプリの登録画面

  6. アプリの情報を記入します
  7. 名前(Name): アプリ名。アプリがユーザーの情報へのアクセス許可を求める際、この名前が表示します。

    サポートされているアカウントの種類(Supported account type): アカウントの種類は二つあります。組織のアカウントと個人のアカウントです。どっちでも使えるように設定します。
    ※組織のアカウントでログインすると、このアカウントが所属する組織のみのアカウントを使える三つ目の種類が追加されます。

    リダイレクトURI (Redirect URI): ログインが成功するとリダイレクトされるURL。BlazorアプリはSPAですので、コンボボックスでSPAを選択し、http://localhost:5000を記入します。リダイレクトURLはアプリのURLと一致しなければなりません。一致しない場合はログイン出来なくなります。

  8. 登録(Register)をクリックして、アプリの登録を終わらせます
  9. アプリケーションの登録

  10. 左のメニューでAPIのアクセス許可(API permissions)を選択し、アクセス許可の追加
    (Add a permission)をクリック。
  11. アクセス許可の追加

  12. APIアクセス許可の要求(Request API permissions)画面でMicrosoft Graphをクリックします。
  13. 委任されたアクセス許可(Delegated permissions)の許可の種類を選択し、検索でmail.readを入れます。
  14. 下に出るMail.Readの許可を設定し、アクセス許可の追加(Add permissions)ボタンで許可設定を終わらせます。
  15. APIアクセス許可の要求

  16. アプリの概要画面に表示するアプリケーション(クライアント)ID (Application (client) ID)を覚えます。BlazorアプリでGraphにアクセスする為に使います。
  17. アプリケーション(クライアント)ID

Graphアセンブリへの参照の追加

HelloBlazor.csprojにGraphアセンブリーへの参照を追加します。ItemGroupの中に新しいPackageReferenceタグを追加し、Include属性値としては Microsoft.Graphとします。

<ItemGroup>
      …
      <PackageReference Include="Microsoft.Graph" Version=3.10.0" />
    </ItemGroup>

MSALライブラリの追加

Graphからユーザーの情報を取得するには、ユーザーが自分のアカウントでBlazorGraphにログインする必要があります。その為にMicrosoft Authentication Libraryが(MSAL)を使います。アプリの一つのページだけでポップアップによりログインを行いたいのでMSALのJavaScriptライブラリーを使います。 Index.htmlには、js/extensions.jsの前にmsal-browser.min.jsへの参照を追加します。

<script type="text/javascript"
    src="https://alcdn.msauth.net/browser/2.14.2/js/msal-browser.min.js"></script>

JSヘルパー メソッドの作成

JavaScriptライブラリーなので、前回と同じくjs/extensions.jsにベルパー メソッドを追加します。

  • Azure AD上のアプリIDでMSALクライアントのオブジェクトを作成します。
  • どんな情報を取得したいかを引き渡してloginPopupでログインポップアップを表示します。
  • loginPopupはPromiseを使っているのでこちらもPromiseを戻します。
    幸いな事にBlazorは問題なくPromiseの戻り値を対応できます。

結果のソースコードは下記の通りです。

// Graphのヘルパー メソッドgraphHelperオブジェクトに集める
window.graphHelper = {
    // ユーザーがGraphを使えるように自分のMicrosoftアカウントにログインする為のメソッド
    : function (_clientId, _scopes, _redirectUri) {
        //MSALのクライアント オブジェクトを作成する為のコンフィグ
        const config = {
            auth: {
                // アプリケーション(クライアント)ID
                clientId: _clientId,
                // ログイン成功時のリダイレクトURL
                // Azureで登録済みのURLに一致する必要がある
                redirectUri: _redirectUri
            }
        };

        // ログイン要求時にGraphでユーザーのどんな情報を取得したいのを設定する
        const loginRequest = {
            scopes: _scopes
        };

        // MSALクライアントをインスタントする
        const _msal = new msal.(config};

        return new Promise((, ) => {
            //ユーザーにログインのポップアップを表示する
             _msal.(loginResponse) {
                .(function (loginResponse) {
                    //ログインが成功したらアクセス トークンが出るので、
                    //それを戻す
                    let accessToken = loginResponse.accessToken;

                    (accessToken);
                })
                .(function (error) {
                    console.(error) {
                    (null);
                });
        });
    }
}

AuthServiceの作成

JavaScriptメソッドを呼び出すにC#側にもヘルパー メソッドを作成します。

public async Task<String> (String[] scopes)
        {
            var token = await js.<String>("graphHelper.Login", appId, scopes, scopes, http.BaseAddress!.AbsoluteUri);
            return token;
        }

最初のパラメーターとしてはJavaScriptメソッド名を引き渡しているが、その他のパラメーターは:
appId: Azure AD上のアプリケーション (クライアント) ID
scopes: どんな情報へのアクセス許可が必要のか
http.BaseAddress!.AbsoluteUri: Blazorアプリで使われているHttpClientからBlazorアプリのURLを取得します。それがリダイレクトURLになります。

HttpClientはIJSRuntimeと同様にDependency Injection (DI)によりクラスに入れます。BlazorでDIの使い方などについては前回に話したので、ここでは結果のコードだけ見せます。

public class AuthService
    {
        // Azure AD上のアプリケーション (クライアント) ID。
        // 実際のアプリではソースコードに置かないでください^^
        private const String appId = "[APPLICATION_ID]";
        private IJSRuntime js;
        private HttpClient http;

        public (IJSRuntime js, HttpClient http)
        {
           this.js = js;
           this.http = http;
        }

        public async <String> (String[] scopes)
        {
           var token = await js.<String>("graphHelper.Login", appId, scopes, http.BaseAddress!.AbsoluteUri);
           return tokan;
        }
    }

MSALAuthProviderの作成

上記のAuthServiceでアクセストークンを取得し、GraphのAPIへのアクセス時に使います。その為にGraphのAuthenticationProviderを作成する必要があります。
AuthenticationProviderはIAuthenticationProviderインターフェースを実装した物です。そのインターフェースはAuthenticateRequestAsyncの一つメソッドがあります。
AuthenticateRequestAsyncメソッドはGraphクライアント オブジェクトがGraphのAPIへのアクセスする前に呼ばれるので、そこでHttpRequestのヘッダーにアクセストークンを設定します。

public async Task (HttpRequestMessage request)
        {
            request. Headers. Authorization = new ("Bearer", await ());
        }

GetAccessTokenには、先ほど作成したAuthServiceを使ってユーザーをログインさせてアクセストークンを取得します。

accessToken = await authService(_scopes);
return accessToken;

このアプリでユーザーのあるメールを表示しますので、scopesとしてはUser.ReadとMail.Readを引き渡します。User.Readはユーザーの基本情報を取得許可で、Mail.Readはユーザーのメールを読み込む許可です。AuthServiceは勿論DIによりクラスに挿入します。
結果は下記の通りです。

// Graphから情報を取得する際、HTTP要求のヘッダーに証明情報を入れるプロバイダー
    public class MSALAuthProvider : IAuthenticationProvider
    {
        private AuthService authService;
        private String[] _scopes;
        private String accessToken = String.Empty;
        private (AuthService authService)
        {
            this.authService = authService;

            // このアプリではユーザーの情報とユーザーのメールを取得する
            _scopes = new[] { "User.Read",  "Mail.Read" };
        }

        // ユーザーのトークンを取得する。
        public async Task<String> ()
        {
            if (String.(accessToken) != true)
                return accessToken;
            
            accessToken = await authService(_scopes);
            return accessToken;
        }

        // Graphにアクセスしようとする時に呼び出されるメソッド
        public async Task (HttpRequestMessage request)
        {
        
            // ヘッダーにユーザーのトークンを入れる
            request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await ());
        }
    }

GraphServiceの作成

GraphへのアクセスはMicrosoft.Graph.GraphServiceClientというクラスで出来ます。作成時にHttpClientのインスタンスを引き渡しますが、新しいインスタンスで良いのでBlazorアプリで使われているHttpClientをDIにより挿入しなくても良いですけど、上記のMSALAuthProviderを挿入します。

this.graphClient = new GraphServiceClient(new HttpClient());
this.graphClient.AuthenticationProvider = msalAuthProvider;

GraphServiceClientで情報取得するには、ユーザーの情報に対してリクエストを作成して実行します。AuthServiceでログインさせたユーザーはMeプロパティーに表れます。基本情報を取得するにはMeプロパティーに対してRequestメソッドでリクエストします。リクエストをGetAsyncメソッドで実行します。リクエストの結果のプロパティーにユーザーの情報が格納されます。例えば、ユーザー名はDisplayNameです。下記のように

var myin = graphClien.Me;
var requestForMyin = myin.();
var requestResult = await requestForMyin.();
var myin = requestResult.DisplayName;

Meには色んなプロパティーがあります。今回はユーザーのメールを取得したいのでMessagesプロパティーを使います。というわけで、メール取得メソッドを作成しましょう。

public async Task<Message[]> (String sender, DateTime fromDate)

引き渡された差出人(sender)の、fromDateの日付以降のメールを戻します。ここの注意事項としてはGraphの時間はUTCである為、日付をGraphのAPIに引き渡す前にUTCに変更する必要があるかもしれません。

var filterDate = fromDate
                    .()
                    .("yyyy-MM-dd");

graphClient.Me.Messages.Request()でメール取得リクエストを作成しますが、全部のメールを取得したくないのでフィルタリングする必要があります。その為にFilterメソッドを使います。

.($"ReceivedDateTime ge {filterDate} and from/emailAddress/address eq '{sender}'")

フィルタリング条件を文字列で設定します。上記のフィルタリング条件の構成を説明します。
ReceivedDateTime: メールの日時
ge: 以降 (Greater than or Equal)
and: 二つ以上の条件を結ぶ構文
from/emailAddress/address: 差出人のメールアドレス
eq: 等 (Equal)

その他のフィルタリング条件の構文に関しては次のページをご参考ください。
※外部サイト:クエリ パラメーターを使用して応答をカスタマイズする

また、メールのどんな項目を取得したいのも設定しなければなりません。ラムダ式で設定できます。

.(m => new { m.Subject, m.Body, m.ReceivedDateTime })

作成したリクエストをGetAsyncで実行します。
FilterやSelectなどはリクエストに対して、変更したリクエストを新しいオブジェクトとして戻すので下記のように呼べます。
Me.Messages.Request().Filter(…).Select(…).GetAsync()

条件に当てはまるメールは結果のCurrentPageプロパティーでMessageの配列として戻されますが、当てはまるメールが多すぎると一部のメールだけ戻されます。その他の当てはまるメールを取得する為のリクエストは結果のNextPageRequestプロパティーにあります。それをGetAsyncで呼び出しても良いです。NextPageRequestはnullであれば、それ以上当てはまるメールがないという事になります。GraphServiceは下記のようになります。

public class GraphService
{
    private GraphServiceClient graphClient;

    public (MSALAuthProvider msalAuthProvider)
    {
      this.graphClient = new GraphServiceClient(new HttpClient());
      this.graphClient.AuthenticationProvider = msalAuthProvider;
    }

    // ユーザー名を取得するメソッド
    public async Task<String> ()
    {
        var me = await graphClient.Me.().();
        return me.DisplayName;
    }

    // fromDate以降のsenderからのメールを取得するメソッド
    public async Task<Message[]> (String sender, DateTime fromDate)
    {
        var messages = new List<Message>();
        var filterDate = fromDate
                                .()
                                .("yyyy-MM-dd");

        va esultPager = await graphClient.Me.Messages
                                        // メール取得要求のオブジェクトを作成
                                        .()
                                        // 文字列のクエリとしてフィルタリングを追加
                                        .($"ReceivedDateTime ge {filterDate} and from/emailAddress/address eq '{sender}'")
                                        // クエリで取得したい項目を設定
                                        .(m => new
                                            m.Subject,
                                            m.Body,
                                            m.ReceivedDateTime
                                        })
                                        {
                                        // ソートの順を設定
                                        .("ReceivedDateTime DESC")
                                        // クエリを実行
                                        .();
        // CurrentPageではクエリの結果の情報が格納する
        messages.(resultPage.CurrentPage);

        // 情報は一つの呼び出しで取得するには多すぎる時、
        // CurrentPageで一部の情報が戻されて、NextPageRequestで続きを取得できるクエリが格納する
        while (resultPage.NextPageRequest != null)
        {
            // NextPageRequestがある限り、それを実行して結果の情報を取得する
            resultPage = await resultPage.NextPageRequest.();
            messages.(resultPage.CurrentPage);
        }

        // 戻す前にメールの日付を現地時間に変換する
        return messages
                    .(m => new Message
                    {
                      Subject = m.Subject,
                      Body = m.Body,
                      ReceivedDateTime = m.ReceivedDateTime?.()
                    })
                    .();
    }
}

DIに登録

先ほど作成したAuthService、MSALAuthProvider、GraphServiceはアプリで使えるようにDIに登録します。前回のLocalStorageのようにProgram.csでbuilder.Servicesに追加します。

builder.Services.<AuthService>();
builder.Services.<MSALAuthProvider>();
builder.Services.<GraphService>();

上記のDI登録には二つのメソッドが使われていますね。ここでDI登録の種類を説明します。
Transient: DIに登録されたサービスは、挿入されるたびに新しいインスタンスが生成されます。
Scoped: DIに登録されたサービスは、ユーザー毎に生成されます。
Singleton: DIに登録されたサービスは、アプリ毎に生成されます。BlazorのWebAssemblyアプリは基本的に一人のユーザーしか使えないので、SingletonはScopedとは変わらないです。

メールを表示するページの作成

メールを表示するページはテンプレートに入っているFetchData.razorを元にして作成しますので、Pages/FetchData.razorをコピーしてStarbucksMails.razorに名前を変更します。
ページについては、説明したいところは余りないので取り敢えずソースコードを見せます。

@page "/starbucks"
@inject Graph.GraphService GraphService
  

<h1>Hello, @name!</h1>
  
@if (mails == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <table class="table">
      <thead>
          <tr>
              <th>Date</th>
              <th>Subject</th>
          </tr>
      </thead>
      <tbody>
          @foreach (var mail in mails)
          {
              <tr>
                  <td>@mail.ReceivedDateTime?.("yyyy-MM-dd HH:mm:ss")</td>
                  <td>@mail.Subject</td>
              </tr>
          }
    </tbody>
</table>
}

@code {
    private Microsoft.Graph.Message[] mails;
    private String name;

    protected override async Task ()
    {
        name = await GraphService.();
        // スタバからの三ヶ月分のメールを取得
        // 自分のアカウントのメールにありえる差出人に変更してください^_^
        mails = await GraphService.("card_admin@mx.starbucks.co.jp", DateTime.Now.(-3));
    }
}

一つだけですけど、前回は分離コードを使ってAspNetCore.Components.Inject属性で依存性を挿入したけど、今回は分離コードを使わずに@injectで依存性をページ直接に挿入します。
AspNetCore.Components.Injectも@injectも結果が同じです。

メールのページをメニューへ追加

全ページの左にあるメインメニューはShared/NavMenu.razorに定義されています。作成したページへのリンクを追加します。

<div class="@NavMenuCssClass" >
    <ul …>
        
      <li class="nav-item px-3">
          <NavLink class="nav-link" href="starbucks">
              <span class="oi oi-list-rich" aria-hidden="true"></span> Starbucks mails
          </NavLink>
      </li>
    </ul>
</div>

NavLinkはBlazorにおいてリンクを表すコンポネントです。結果として<a href/>になります。

結果

コマンドプロンプトにdotnet runを実行して、ブラウザーでhttp://localhost:5000に移動します。メインメニューにあるStarbucks mailsをクリックします。

結果イメージ

初めてGraphにアクセスする際は下記のページが表示します。自分が作成したアプリだと確認の上、Yesをクリックして自分のアカウントの情報へのアクセスを許可してください。

結果イメージ

勿論、メールの日時と件名を表示するだけで意味があまりないです。皆さん気づいたかもしれませんが、メールを取得するリクエストを作成した時にSelectのラムダ式でメールのBodyも取得するように設定しました。実際のところ、個人のアプリでスタバからのメールの本文から入金の金額を取り出して個人会計に使いますが、それについてまたいつか...

当サイトの内容、テキスト、画像等の転載・転記・使用する場合は問い合わせよりご連絡下さい。

エバンジェリストによるコラムやセミナー情報、
IDグループからのお知らせなどをメルマガでお届けしています。

メルマガ登録ボタン

ベロフ ドミトリー

株式会社インフォメーション・ディベロプメント 先端技術部 エバンジェリスト

この執筆者の記事一覧

関連するナレッジ・コラム

導入必須!災害発生時に有効なタイムライン(防災行動計画)とは?

話題のRPAツールPower Automate Desktopに触れてみよう

実は脆弱?日本のサイバー能力と置かれたポジションを認識する