Microsoft Graph API Schnellstart Teil 2 Planner mit ASP.NET core

Nach Teil 1 folgt nun ein komplexeres Beispiel. Ziel ist es Auswertungen zur Nutzung eines Teams Planner Daschboards zu erstellen. Microsoft Teams selber hat nur relativ wenig Möglichkeiten. Mir geht es um eine einfache Fragestellung: Wer legt wie viele Service Requests in unserer IIT Teams Gruppe an. Die Erfahrung zeigt, wer kaum das Tooling  nutzt, löst seine Probleme auf Umwegen.

Ab in den Graph Explorer. Erst mal die Möglichkeiten checken, wie man an Planner Tasks eigentlich rankommt. In den vorgefertigen Sample Querys ist auch was passendes drin. “all Planner Tasks for a plan” liefert im Query Eingabefeld die API Url https://graph.microsoft.com/v1.0/planner/plans/{plan-id}/tasks.

Wo bekommt man nun die Plan-id (PlannerId) genau her. Aus Teams schon mal nicht.

Methode #1: Der Planner Hub https://tasks.office.com/. Dort auf den gewünschten Plan /Gruppe auswählen. Nun die Url kopieren, die bei unserem ppedv service request Plan so aussieht

https://tasks.office.com/ppedv.de/de/Home/Planner/#/plantaskboard?groupId=ee0a1be0-1310-4c6a-af5e-8754f6862a25&planId=qn-MTLQmEkuo_l6_9JRt5JYBBF7t

Die PlanId rauskopieren also das beginnend mit qn-….

Methode #2: eine Abfrage per Graph Explorer die einen passenden Task liefert. In meinem Fall habe ich eine Task im fraglichen Board angelegt. Planner- “all my Planner Tasks”

https://graph.microsoft.com/v1.0/me/planner/tasks

image

Da ist sie wieder meine PlanId qn-….

Damit lassen sich nun alle Tasks im gewünschen Kanban Board (=Planner) auslesen.

image

ich hatte im ersten Teil schon ein wenig über Filter geschrieben. Wie man hier sehen kann, sind in unserem Planner (in allen Buckets) fast 400 Tasks.

image

Bei Planner Tasks ist die API Grenze 400, bei User Objekten 100. Wenn es mehr als diese Anzahl gibt, liefert die API (willkürlich) 400 und einen Next Link, der die nächsten Ergebnisse zeigt. Ein Filter auf Datum oder Top wie es Odata vorsieht wird einfach ignoriert.

Betrachten wir die Json Daten von der Microsoft Graph API genauer.

image

Eigentlich würde ich gerne die Anzahl der Tasks per User wissen. Unter createdBy.User.displayname steht aber nichts. Ist leer, nur die kryptische User ID wird übermittelt. Auch hier gilt das Odata theoretische per $include Subquerys anbieten würde, aber nur wenn es die API tut. Und bei Planner Tasks, no way.

Meiner Meinung nach muss ich einen extra Query für den User absetzen. Damit ich das nicht x-mal tun muss, holt man einfach alle User (ID + Displayname) und matcht später mal.

Also wieder in den Graph Explorer Sample Querys- User- “all users in the organization”. Eigentlich sind das nur knapp unter 50.

https://graph.microsoft.com/v1.0/users

Und was kommt? Unbekannte 100 Menschen und mystischer Nextlink.

image

Was macht Andreas da? Ich vermute, da wir unsere Trainings virtuell mit Teams durchführen, haben sich die in unsere Organisation gemogelt.

Lösung ich filtere auch email Domain. Das geht bzw man findet sogar einen halbwegs passende Vorlage

"users with displayName containing ‘room’, mail ending with ‘microsoft.com’, and sorted by name"

Ganz oft findet man in Foren und der Doku das Endwith nicht funktioniert. Das stimmt auch in gewisser Weise. Wenn man die Query so tippt, wird man scheitern. StartsWith klappt, Endwiths nicht.

image

Das liegt an einem zusätzlichen Request Header. Löscht man den raus wird folgende Query

https://graph.microsoft.com/v1.0/users?$count=true&$search="displayName:room"&$filter=endsWith(mail,'microsoft.com')&$orderBy=displayName&$select=id,displayName,mail

einen Fehler in den JSON Result samt Statuscode 400 Bad Request liefern

"error": {
"code": "Request_UnsupportedQuery",
"message": "Unsupported Query.",

Außerdem ist die $count Odata Query notwendig! Dann gestalte ich die MS Graph API Query so um, damit wir ppedv Mitarbeiter in Deutschland und Österreich finden. Dafür habe ich übrigens den Windows Editor (notepad) offen um dort diese zwischenschritte abzulegen.

https://graph.microsoft.com/v1.0/users?$count=true&$filter=endsWith(mail,'ppedv.de') or endsWith(mail,'ppedv.at')&$select=id,displayName,mail

Schon besser den Andi kenn ich doch

image

Auf gehts zum C# Code der ASP.NET Seite. Ich verwende mein Projekt aus Teil 1 und ergänze mit einer neuen Razor Page socialindex. Die nötigen HTML View Anpassungen in socialindex.cshtml sind in Teil1 erläutert.

   1:  @page
   2:  @model WebApp_OpenIDConnect_DotNet.pages.socialindexModel
   3:  @{ Layout = "_Layout"; }
   4:   
   5:  <h1>Social Index</h1>

Das MSGraph Objekt in der socialindex.cs Datei per DI Constructor Injection referenzieren. Da Microsoft Graph API Calls längern dauern, ist das SDK asynchron ausgelegt. Am Ende sollte folgender Rumpf Code die Basis für weitere Aktivitäten werden.

1:public class socialindexModel : PageModel
2:{
3:  GraphServiceClient graphClient;
4:  public socialindexModel(GraphServiceClient _graphClient)
5: { 
6:    graphClient = _graphClient;
7: }
8:  public async Task<ActionResult> OnGet()
9: {
10:   return Page();

 

Nun gehts dem User und Task Listen an den Kragen. Womit fangen wir an? Ganz hinten mit der Darstellung im View. Dazu erzeuge ich erst eine Klasse die dann als generische Liste gebunden werden kann.

   1:  public class TeamsAktion
   2:  {
   3:    public string DisplayName { get; set; }
   4:    public string Mail { get; set; }
   5:    public int Anzahl { get; set; }
   6:  }

Diese Klasse wird in der Datei SocialIndex.cs als Listen Property angelegt und initalisiert.

 public List<TeamsAktion> Liste { get; set; } =
new List<TeamsAktion>();

Genug Housekeeping. Holen wir uns die User, der ppedv. Die Querystring Optionen aus unserem Graph Explorer Versuchen, werden als eine Liste von Options angelegt. Es gibt unterschiedliche Type (Header, Query) und zum Schlüssel einen Wert.
Auf dem GraphClient wird dann die User API aufgerufen, dem Request die Options mit übergeben und brav async aufs Result gewartet.

   1:  var options = new List<Option>()
   2:  options.Add(new QueryOption("$filter",
"endsWith(mail,'ppedv.de') or endsWith(mail,'ppedv.at') "));
   3:  options.Add(new HeaderOption("ConsistencyLevel", "eventual"));
   4:  options.Add(new QueryOption("$count", "true"));
   5:  var tmp = await graphClient.Users.Request(options).GetAsync();
   6:  var Userlist = tmp.ToList();

Kompiliert? Dann nichts wie mal ans ausführen. F5 in Visual Studio, die URL aufrufen und peng

An unhandled exception occurred while processing the request.

ServiceException: Code: Authorization_RequestDenied
Message: Insufficient privileges to complete the operation.

Die Sache mit den Rechten (Authorization) muss auf App Ebene für die API definiert werden. Wir erinnern uns daran, jede Art von API Call hat eigne Recht, hier als Scope bezeichnet. Ich hoffe das Azure Portal ist noch geöffnet sonst ab zu den Details der App Registrierung

https://portal.azure.com/#blade/Microsoft_AAD_RegisteredApps/ApplicationsListBlade

Im Azure Portal bei meiner vorher angelegten App - Menü Api Berechtigungen- Berechtigung hinzufügen und aus den Katalog Microsoft Graph auswählen.

image

Weiter gehts mit delegierte Berechtigungen. Im Reiter User findet sich passende Scopes. Die Checkbox bei User.Read.All aktivieren.

image

Unterhalb der Liste findet sich der Button Berechtigung hinzufügen. Mutig drauf drücken. Die Einwilligung kann als Admin auch per Portal für alle Benutzer gesetzt werden. Ich mache das und erteile die Administratoreinwilligung

image

Nun müssen Sie die Anwendung neu starten und dann auf die SocialIndex Seite wechseln. Es kommt kein Fehler mehr. Um die Liste zu überprüfen eignet sich ein Breakpoint und der Visual Studio Debugger. Es sind Einträge drin. Passt!

Für den Abruf der Planner Tasks reicht eine Zeile C# Code auf dem Graphclient. Die Idee stammt aus der URL mit dem Graph Explorer.

var pTasks = await graphClient.Planner.Plans["qn-MTL….."]
.Tasks.Request().GetAsync();

Allerdings auch hier wird ein Scope benötigt. Dafür wieder in das Azure Portal wie zuvor und die API Berechtigung Tasks Read anhaken.

image

Restliches Prozedere wie zuvor!

Auf geht es in den Endspurt. Für den erfahrenen ASP.NET Entwickler ein Klacks, die Daten per LINQ zu aggregieren und dann beide Tabellen User und Tasks zusammen führen.

   1:   var TaskGrouped = from t in pTasks
   2:       group pTasks by t.CreatedBy.User.Id into items
   3:       select new { Key = items.Key, Anzahl = items.Count() };
   4:   
   5:  foreach (var item in TaskGrouped.OrderByDescending(x => x.Anzahl))
   6:  {
   7:    var u = Userlist.Where(x => x.Id == item.Key).FirstOrDefault();
   8:    if (u != null)
   9:     {
  10:       Liste.Add(new TeamsAktion() { DisplayName = u.DisplayName,
Mail = u.Mail, Anzahl = item.Anzahl });
  11:     }
  12:  }

Noch lieber arbeite ich mit der Razor Syntax um die Visualisierung auch mit Bootstrap CSS Klassen zu realisieren. Auf in die .cshtml Datei

   1:  <ul class="list-group">
   2:      @foreach (var item in Model.Liste)
   3:      {
   4:          <ul class="list-group-item">
   5:              @item.DisplayName
   6:              @item.Mail
   7:              <span class="float-right">@item.Anzahl</span>
   8:          </ul>
   9:      }
  10:  </ul>

So dann sind wir fertig. Hat nicht immer Spaß gemacht, aber funktioniert.

Kommentare sind geschlossen