MS Graphでユーザー追加とライセンス付与

またまた、MS Graphの話で申し訳ないのだが、チョット嵌った点があったので、メモ。

MS Graphを使用して、Office365のユーザーを追加するプログラムを書いてみた。MSのドキュメントにはユーザー作成時にライセンスを付与できるとも、できないとも書いていないので、一応、ライセンス付きで試してみたが、NG。(Bing AI君に聞いてみたら、「ユーザー作成時にはライセンス付与できません。」と言われた。「ドキュメントにはハッキリとは書いてありませんが・・・」だと。AI君が正しいかどうかは分からんが。)

仕方がないので、ユーザーを追加してから、ライセンスを付与する形にしてみた。

以下動作したプログラム

//
// ユーザーの作成とライセンス付与
// どうも、ユーザー作成時にライセンスを付与することはできないようだ。
//	(ドキュメントには明記されていないようだが・・・)
// 仕方ないので、ユーザーを作成して、その後、ライセンスを付与することとする。
// ちなみに、こちらもドキュメントには書かれていないが、ユーザー作成時にはUsageLocationと
// PreferredLanguageは必須!
// これがないと、ユーザーをEnableにできないし、ライセンス付与もできない。
//
var cred = new UsernamePasswordCredential(ManageUser,ManagePass,TenantID,AppID,options);
var cli = new GraphServiceClient(cred);

var request = new User() {
    UserPrincipalName = "boo@foo.com",
    MailNickname = "boo",
    AccountEnabled = true,
    DisplayName = "Boo.Foo(Mr.Boo)",
    Surname = "Foo",
    GivenName = "Boo",
    PasswordProfile = new PasswordProfile() {
        Password = MakePassword(),
        ForceChangePasswordNextSignIn = true
    },
    UsageLocation = "JP",	// 必須!
    PreferredLanguage = "ja-JP",	// 必須!
};
var res = await cli.Users.PostAsync(request);

// ユーザー追加要求後すぐにライセンス付与を行おうとするとエラーになったので、
// とりあえず、1秒程まってから、ライセンスを追加する。
Thread.Sleep(1000);
var licreq = new Microsoft.Graph.Users.Item.AssignLicense.AssignLicensePostRequestBody() {
    AddLicenses = new List<AssignedLicense>() {
        new AssignedLicense() {
            DisabledPlans = new(),  // ←これは無くても大丈夫
            SkuId = Guid.Parse("XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX")
        }
    },
    RemoveLicenses = new()  // ←削除するライセンスが無くても空配列が必要なようだ。
};
await cli.Users["boo@foo.com"].AssignLicense.PostAsync(licreq);

プログラムコメントにも書いているが、ユーザーの属性として、UsageLocationとPreferredLanguageは必須。(MSのGraph APIドキュメントには書いてない)

ライセンス付与の方だが、MSのサンプルはMe.AssignLicense.PostAsyncを使用した例のみが載っていて、ユーザー指定のライセンス付与の例は載っていなかったのだが、使用するRequestBodyのクラス名が異なるだけで、内容は同じようだ。

AddLicensesはAssignedLicenseクラスのリストで、ここに付与するライセンスを列挙する。多分、DisabledPlansは使用することはほぼないだろうと思われる。SkuIdはライセンスのSkuId(以前の記事で取得できる、SubscribedSkuクラスのメンバー)を指定する。

もちろん、複数のライセンスを付与することが可能。

ちなみに、RemoveLicenseは付与されているライセンスを削除するためのものだが、もちろん、今回は使用しない(というか削除するライセンスが無い)。削除するライセンスが無くても、空配列(C#上はListだが)を指定しないと実行時エラーとなる。(ここも嵌まった点)

後はcli.Users[UserPrincipalName].AssignLicense.PostAsync()に作成したインスタンスを渡せば、ライセンスが付与される。

ちなみに、プログラムコメントに書いたように、ユーザー作成⇒ライセンス付与をDelay無しで行なうと、実行時エラーとなってしまった。とりあえず、1秒置いてからライセンス付与をしているが、実際の所、どの程度の時間が必要なのかは不明。作成したユーザーを別スレッドで検索して検索OKまで待つような処理が必要かも。

ちなみに、必要な特権はドキュメント上は下記となる。使用しているAzureアプリには色々と特権付けているので、本当にこれだけで大丈夫か分からんが・・・

  • User.ReadWrite.All
  • Directory.ReadWriteAll

カテゴリー: .NET, C#, Microsoft Graph, 技術系 | 1件のコメント

MS Graphでメール送信

この所、MS Graphの話ばかりとなってしまっているが、今回もMS Graphのお話。

今回は、メール送信について。今まで、いくつかメール送信のプログラムを書いてきたが、大抵、MailKit.Net.Smtpを使用し作成していた。

MailKit.Net.Smtpを使用して、Office365経由でメールを送信する場合は、Access Tokenを取得後SaslMechanismOAuth2を作成して、認証するわけだが、結構面倒だ。

調べてみると、MS Graphだけでもメール送信ができるようなので、試してみた。

Azureアプリケーションに必要な権限は

  • Mail.Send

のみ。

メッセージを作成後、いつものごとく、MS Graph Clientインスタンスを作成して、Me.SendMail.PostAsyncメソッドを呼び出すだけ。

// MS Graph Clientインスタンスの作成
var cli = new GraphServiceClient(cred);
// メッセージの作成
var mesg = new Microsoft.Graph.Me.SendMail.SendMailPostRequestBody() {
    Message = new Message() {
        Subject = "メール送信テスト",
        Body = new ItemBody() {
            ContentType = BodyType.Text,
            Content = "メール送信てすとだよぅ"
        },
		// To
        ToRecipients = new List<Recipient>() {
            new Recipient() {
                EmailAddress = new EmailAddress() {
                    Address = "boo@foo.com"
                }
            }
        },
		// Cc
        CcRecipients = new List<Recipient>() {
            new Recipient() {
                EmailAddress = new EmailAddress() {
                    Address = "foo@woo.com"
                }
            }
        },
		// 添付
        Attachments = new List<Attachment>() {
            new FileAttachment() {
                Name = "hoge.png",
                ContentType = "image/png",
                ContentBytes = buff
            }
        }
    },
    SaveToSentItems = true
};
// メール送信
await cli.Me.SendMail.PostAsync(mesg);

カテゴリー: .NET, C#, Microsoft Graph, 技術系 | コメントする

Office365個人に割当てられているライセンス取得

前回の記事では、テナントに割当てられている、ライセンス一覧を取得してみたが、今回は個人に割当てられている、ライセンスを取得してみる。

取得するには、アプリケーションに下記の権限が必要。

  • User.Read
  • User.Read.All
  • User.ReadWrite.All
  • Directory.Read.All
  • Directory.ReadWrite.All

※これも何故Writeが必要なのだろう??

個人に割当てられているライセンスを取得するには、下記のようなコードを書けばよい。

// Graph Client
var cli = new GraphServiceClient(cred);

// サインインしているユーザーのライセンス取得
var lic = await cli.Me.LicenseDetails.GetAsync();

// 指定ユーザーのライセンス取得(権限が必要)
var lic = await cli.Users[UserPrincipalName].LicenseDetails.GetAsync();

List<LicenseDetails?>がlic.Valueに返される。LicenseDetailsの内容だが、必要なのはSkuPartNumberだろう。これは、前回の記事と同様の文字列である。

以下実行例

// Graph Client
var cli = new GraphServiceClient(cred);
// 指定ユーザーのライセンス取得(権限が必要)
var lic = await cli.Users[UserPrincipalName].LicenseDetails.GetAsync();
if (lic?.Value == null) {
	Console.WriteLine("取得失敗");
	return;
}

Console.WriteLine($"{UserPrincipalName}には以下のライセンスがアサインされている");
foreach(var v in lic!.Value!) {
    Console.Write($"{v.SkuPartNumber},");
}
Console.WriteLine();
/* 実行例
boo@foo.comには以下のライセンスがアサインされている
OFFICESUBSCRIPTION,EXCHANGESTANDARD,
*/
カテゴリー: .NET, C#, Microsoft Graph, 技術系 | コメントする

MS Graphでテナントのライセンスを取得

Powershell Office365モジュールでは、簡単にテナントのライセンス情報を取得できるのだが、プログラムからMS Graphで取得するにはどうすればよいか、調べてみた。

調べ方が悪いのか、あまり例がヒットしなかったが、以下のような感じで取得できることを確認した。

まず、Azure AD上のアプリケーションに必要な、アクセス許可は以下の通り。

  • Origanization.Read.All
  • Directory.ReadAll
  • Organization.ReadWrite.All
  • Directory.ReadWrite.ALl

なぜ、Writeが必要なのかは不明・・・

取得方法だが、GraphへアクセスするためのServiceClientを作成して、.SubscribedSkus.GetAsync()メソッドを呼び出すだけ。

戻り値として、契約しているライセンス情報一覧が返される。

// GraphServiceClientの作成(Credentialの作成方法は省略)
var cli = new GraphServiceClient(cred);
// 契約しているライセンス情報一覧の取得
var lic = await cli.SubscribedSkus.GetAsync();

上記の場合、lic.ValueにList<SubscribedSku>?が入ってくる。SubscribedSkuには様々な情報が入ってくるが、特によく使うのは下記ぐらいだと思われる。

SkuPartNumberライセンス名ENTERPRISEPACKのような表記
ConsumedUnits使用ライセンス数
PrepaidUnits.Enabled購入数
主なプロパティ

この情報が取れれば、どのライセンスを何ライセンス契約していて、使用数が何ライセンスか分かり、計算すれば割当可能数も取得できるので、結構便利。

※SkuPartNumberはここに一覧がある。

以下実行例

GraphServiceClient cli = new GraphServiceClient(cred);
var lic = await cli.SubscribedSkus.GetAsync();
if (lic == null) {
    Console.WriteLine("Get License Fail");
    return;
}
if (lic.Value == null) {
    Console.WriteLine("Get License Fail");
    return;
}
foreach(var itm in lic.Value) {
    int all = (int)itm.PrepaidUnits.Enabled!;
    int use = (int)itm.ConsumedUnits!;
    Console.WriteLine($"{itm.SkuPartNumber},{all},{use},{all-use}");
}
/* 実行例
STREAM,XXXX,YY,MMMM
Dynamics_365_Sales_Premium_Viral_Trial,XXXX,YY,ZZZZ
WINDOWS_STORE,XXXX,Y,ZZZZ
ENTERPRISEPACK,XX,YY,ZZ
FLOW_FREE,XXXX,YY,ZZZZ
MICROSOFT_BUSINESS_CENTER,XXXX,Y,ZZZZ
POWERAPPS_VIRAL,XXXX,Y,ZZZZ
EXCHANGESTANDARD,XXX,YY,ZZ
POWER_BI_STANDARD,XXXX,Y,ZZZZ
OFFICESUBSCRIPTION,XXX,YYY,ZZ
Power_Pages_vTrial_for_Makers,XXXX,Y,ZZZZ
STANDARDPACK,XXX,YYY,ZZ
*/
カテゴリー: .NET, C#, Microsoft Graph, 技術系 | コメントする

Javascriptを使用して数値を通貨フォーマット化

Webアプリを作っていると、UIの作りが面倒な事がままある。UIライブラリ等を使用すれば良いのだろうが、調べたり、慣れたりするまでには意外と手間が掛かってしまう。

今回はあえてJavascript標準ライブラリのみを使用して、入力した数字を通貨フォーマットに変換してみたいと思う。

試してみるのは、<input type=”number”/>のフィールドに入力中に、横に千円単位で区切られた文字列を表示すること。

ちょっとググって(と言っても、bingなのだが・・・)みると、いくつかの方法が出て来た。その中で、1番使い勝手が良さそうなものをチョイスした。

以下のような感じで入力値を通貨形式に変換できるようだ。

var formattedval = Intl.NumberFormat('ja-JP', {style: 'currency', currency: 'JPY', minimumFractionDigits: 0}).format(val);

パラメータにはロケールと変換オプションを指定する。
ロケールはja-JPやen-US等の文字列。
オプションは

  • style
    • ‘decimal’ 10進数
    • ‘currency’ 通貨表示
    • ‘percent’ パーセント
  • currency 通貨の種類’JPY’,’USD’など
  • minimumFractionDigits 小数点以下の表示桁数

これを使用して、入力した値をリアルタイムで表示するようにコードを書いてみた。

<form>
    金額:
    <input type="number" style="text-align:right" id="Price"/>  
    <span style="font-weight:bold" id="FormattedPrice"></span><br/>
</form>
<script>
    $(document).ready(
        ()=>{
            $('#Price').on("input",
                (e)=>{
                    if (e.target.value.length != 0) {
                        var val = parseInt(e.target.value);
                        var formattedval = Intl.NumberFormat('ja-JP', {style: 'currency', currency: 'JPY', minimumFractionDigits: 0}).format(val);Intl.NumberFormat('ja-JP', {style: 'decimal', minimumFractionDigits: 0}).format(val);
                        $('#FormattedPrice').text(formattedval);
                    } else {
                        $('#FormattedPrice').text('');
                    }
                }
            );
        }
    );
</script>

実行結果はこんな感じ

何かに使えそうなのでメモ。

カテゴリー: javascript, Web, 技術系 | コメントする

blazor oninputイベント

blazorで<input …>コントロールの内容が変更された時のイベントとして、onchangeとoninputがあるが、onchangeはコントロールから、フォーカスが外れたときに発生するため、文字の入力中や変更中の値を取得することができない。

これに対して、oninputイベントは文字や値が変更されると即時に発生するため、入力中の文字や変更中の値をリアルタイムに取得することができる。

以下サンプル

@page "/change"

<table>
    <tr>
        <td>onchange:</td><td><input type="text" @onchange="TextChanged"/></td>
    </tr>
    <tr>
        <td>oninput:</td><td><input type="text" @oninput="TextChanged"/></td>
    </tr>
</table>
<br/>
Input Text:@Message

@code {
    protected string? Message;
    protected void TextChanged(ChangeEventArgs e) {
        Message = e.Value?.ToString();
    }
}

忘れない様にメモ。

カテゴリー: .NET, asp.net core, Blazor, C#, 技術系 | コメントする

列挙体メンバーを文字列としてDBに保存

Entity Frameworkで列挙体をメンバーとして持つEntityをコードファーストにて、テーブルを作成すると、列挙体部分は整数(int)型となる。列挙体中の並びに変更が無ければ問題にならないのだが、DBに値を保存した後で列挙体の順番が変更されてしまうと齟齬が発生する。

列挙体への項目挿入などの変更が考えられる場合には、DB中に列挙体メンバー名を文字列化して保存した方が良いかもしれない。

このような値変換を行うには、DbContextから派生させたクラスで、OnModelCreatingをオーバーライドして、Entity中のプロパティについて値コンバーターを定義すればよい。

// Alert Level
public enum AlertLevel { Debug, Information, Warning, Error, Fatal }
// Message
public class AlertMessage {
    [Key]
    public int MessaegId { get; set; }
    public AlertLevel Level { get; set; }
    public string Message { get; set; } = null!;
    public DateTime LogDate { get; set; }
}
// DbContext
public class ConvTestCtx : DbContext {
    public DbSet<AlertMessage> Messages { get; set; } = null!;
		・・・
    protected override void OnModelCreating(ModelBuilder modelBuilder) {
        modelBuilder
            .Entity<AlertMessage>()
            .Property(e => e.Level)
            .HasConversion(
                v=>v.ToString(),    // Instance⇒DB
                v=>(AlertLevel)Enum.Parse(typeof(AlertLevel),v) // DB⇒Instance
            );
    }
}

これで、生成されるテーブル作成SQLは以下の通り。

CREATE TABLE [Messages] (
    [MessaegId] int NOT NULL IDENTITY,
    [Level] nvarchar(max) NOT NULL,
    [Message] nvarchar(max) NOT NULL,
    [LogDate] datetime2 NOT NULL,
    CONSTRAINT [PK_Messages] PRIMARY KEY ([MessaegId])
);

ご覧のとおり、Levelがnvarchar(max)となっている。実際に登録した結果は以下の通り。

MessaegId Level	       Message 	           LogDate
5	  Information  System has Started  2023/03/24 08:53:15
6	  Warning      Low Battery	   2023/03/24 08:53:17
7	  Debug	       Debug Message	   2023/03/24 08:53:18

てな感じで、双方向の型変換が可能。

カテゴリー: .NET, Entity Framework, 技術系 | コメントする

RazorページでBlazorコンポーネントを使用する

Blazorプロジェクト中のRazorページ(.cshtml)等でBlazorコンポーネントを使用したい場合には、どうすればよいのかチョット気になったので調べてみた。

結果としてはTagHelperライブラリの<component>タグを用いるらしい。
詳細はこちらを参照。

componentタグにはレンダリングするTypeとレンダリングの方法を指定する。
(パラメータも指定可能)

おなじみのCounterコンポーネントを使ってみる

@page
@addTagHelper *,Microsoft.AspNetCore.Mvc.TagHelpers
@using blazorApp7.Pages

<html>
    <head>
        <title>Razorコンポーネントテスト</title>
        <link rel="stylesheet" href="css/bootstrap/bootstrap.min.css" />
        <link rel="stylesheet" href="~/css/site.css" />
        <script type="text/javascript" src="~/scripts/project.js"></script>
    </head>
    <body>
        <div class="areabody">
        <h3>CSHTMLからBlazorコンポーネントは使えるのか?</h3><br/>
            <div class="component-area">
                <component type="typeof(Counter)" render-mode="ServerPrerendered"/>
            </div>
        </div>
        <br/>
        <hr/>
        <div class="areabody">
            結論から言うと、Blazor用のJavaScriptおよびその他コンポーネント実行に必要なJavascriptとCSSを読み込めば大丈夫な模様。<br/>
            BlazorServerの場合は"_framework/blazor.server.js"<br/>
            <script src="_framework/blazor.server.js"></script>
        </div>
    </body>
</html>
実行例

Blazorサーバーの場合はサーバーとの通信がNGになると、ページ全体がダメになるのは仕方ないよね・・・

サーバー止めた

用途として、動的View的なものに使えないかと思ったけど、Razorページからコンポーネントへのリアルタイム通知ができないとダメだね。無理やり作ろうと思えばできないことは無いけど、やはり単純にBlazor内で完結させた方がよさそうだ。

カテゴリー: .NET, asp.net core, Blazor, C#, 技術系 | コメントする

List.ContainsやIndexOfの比較を値で行う

List<T>などでTが参照型の場合、ContainsやIndexOfは値(オブジェクト内容)ではなく、参照先を判定する仕様となっている。

これを、値(オブジェクト内容)で判定させたい場合はどうするのかというと、T型にIEquatable<T>インターフェースをインプリメントして、Equalsメソッドを定義して上げれば良い。

// ノーマルバージョン
public class Person {
	public string Name { get; set; }
	public string Mail { get; set; }
}
var p = new Person() { Name = "T.Sumomo", Mail = "t.sumomo@momo.com" };
var p1 = new Person() { Name = "T.Sumomo", Mail = "t.sumomo@momo.com" };
p == p1
p.Equals(p1)
List<Person> l1 = new List<Person>() { p };
l1.Contains(p1)
l1.IndexOf(p1)
// 値比較バージョン
public class Person1 : IEquatable<Person1> {
	public string Name { get; set; }
	public string Mail { get; set; }
	// 値で比較
	public bool Equals(Person1 other) {
		return other?.Name == Name && other?.Mail == Mail;
	}
}
var p2 = new Person1() { Name = "T.Sumomo", Mail = "t.sumomo@momo.com" };
var p3 = new Person1() { Name = "T.Sumomo", Mail = "t.sumomo@momo.com" };
p2 == p3
p2.Equals(p3)
List<Person1> l2 = new List<Person1>() { p2 };
l2.Contains(p3)
l2.IndexOf(p3)
> p == p1
false
> p.Equals(p1)
false
> l1.Contains(p1)
false
> l1.IndexOf(p1)
-1
>
> p2 == p3
false
> p2.Equals(p3)
true
> l2.Contains(p3)
true
> l2.IndexOf(p3)
0

と、このように値で判定を行う事ができる。

もちろん、内容全部を比較するのでは無く、キーとなるプロパティだけの比較とかでも問題は無い。
ちなみに、Equalsではなく、== operatorをEqualsと同じ内容にしても、ContainsやIndexOfは参照先の比較になってしまう。

まぁ、わざわざクラスに手を入れないで、LINQ使ってもいいのだけど・・・

カテゴリー: .NET, C#, 技術系 | コメントする

blazor Dynamic Component

.NET6から、blazorのタグとして、<DynamicComponent>がサポートされた。

このタグはrazorコンポーネントを動的に指定して表示するものであり、ページ内で、表示するコンポーネントの切替などを行う事を想定していると思われる。

使い方は以下のような感じ

@page "/"

<PageTitle>Dynamic Component Test</PageTitle>
<br/>
<p>
    <DynamicComponent>のテスト<br/>
    下記からコンポーネントを選んで、そのコンポーネントを表示します。
</p>
コンポーネント:
<select @onchange="OnDropDownChange">
    <option value=""> </option>
    <option value="@nameof(Counter)">Counter</option>
    <option value="@nameof(FetchData)">Fetch Data</option>
</select><br/>
<hr/>
<div class="ComponentBody">
@if (ComponentType != null) {
    <DynamicComponent Type="@ComponentType" Parameters="@Params" />
}
</div>
@code {

    protected Type? ComponentType = null;
    protected IDictionary<string,object>? Params;

    protected void OnDropDownChange(ChangeEventArgs e) {
        if (e.Value?.ToString()?.Length != 0) {
            ComponentType = Type.GetType($"BlDynamic.Pages.{e.Value}");
            if (e.Value?.ToString() == nameof(Counter)) {
                Params = new Dictionary<string,object>() { {"InitialCount",5} };
            } else {
                Params = null;
            }
        } else {
            ComponentType = null;
        }
    }

}

表示するコンポーネントをselectタグで選んで、そのコンポーネントを表示する簡単な例となっている。使用しているコンポーネントはblazorプロジェクトを作成すると、自動的に生成される、Counter.razorとFetchData.razor。

Counterの方にはInitialCountと言うパラメータを追加して、この呼出し例では5を設定している。

@page "/counter"

<PageTitle>Counter</PageTitle>

<h1>Counter</h1>

<p role="status">Current count: @currentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {
    [Parameter]
    public int InitialCount { get; set; } = 0;
    private int currentCount = 0;

    protected override void OnInitialized()
    {
        base.OnInitialized();
        currentCount = InitialCount;
    }
    
    private void IncrementCount()
    {
        currentCount++;
    }
}
実行例1
実行例2
実行例3

このように、動的に表示するコンポーネントを変更することが可能。

何かに使えるかもしれないので、メモ。

カテゴリー: .NET, asp.net core, Blazor, C#, 技術系 | コメントする