Series · Intune Assignment Viewer: from feature gap to implementation · Part 2 of 2
Microsoft Intune · Microsoft Graph · Blazor Server · .NET 10 · Managed Identity · Application Architecture
Intune Assignment Viewer: Graph access and caching
How Intune Assignment Viewer separates sign-in from Graph access and uses caching, batching and retries to collect assignments across policy families.
- Drafted
- Published
- Updated
- Reading time
- 8 min
- Author
- Roberto Gramellini
Source:
Intune Assignment Viewer
· revision 963b5cc8
- Intune Assignment Viewer architecture
- Microsoft Graph beta
- Managed Identity
- Graph batching
- Graph throttling
- Blazor Server
- Intune application permissions
Intune Assignment Viewer: Graph access and caching
“All assignments for this group” sounds like the title of one query. It is a much larger request. Intune spreads the data across Graph resources with separate catalogs, assignment endpoints, metadata and occasional special cases.
Intune Assignment Viewer does the collection and mapping on the server. Administrator sign-in and Graph access use separate identities. Those choices explain much of the architecture, including the permissions and performance work you need to think about before hosting it.
A Blazor Server application with two distinct trust paths
The application uses ASP.NET Core 10 and interactive Blazor Server components. The browser connects over HTTPS and server-side services call Graph. Application credentials for Graph stay off the browser, where I'd prefer them to remain.
There are two separate identity concerns:
- Administrator sign-in and authorization. Microsoft.Identity.Web uses an Entra ID app registration for OpenID Connect sign-in. The portal requires the configured application role—
IntuneReaderby default—before a user can access the assignments page. - Application-to-Graph authentication. In Azure App Service, the application uses Managed Identity. For on-premises hosting, the repository supports a separate Graph app registration using a certificate or, as a less secure fallback, a client secret.
With this split, the sign-in registration needs no application permissions for Graph reads. On premises, protecting the separate Graph credential becomes a host responsibility. The README prefers certificates to client secrets and warns against putting credentials in appsettings.json.
Grant the Graph application the permissions its supported Intune resources require, with admin consent. The repository lists examples including Group.Read.All, DeviceManagementConfiguration.Read.All, DeviceManagementApps.Read.All, DeviceManagementServiceConfig.Read.All, and DeviceManagementScripts.Read.All. I'd check the least-privilege set against both enabled features and Microsoft's current permission documentation, rather than copy the list without review.
Treat IntuneReader as a privileged, tenant-wide read role. The signed-in user's app role gates access to the portal, but Graph calls are made with the application's Graph identity and its tenant-wide application permissions—not with that user's delegated Graph permissions. A user granted IntuneReader can therefore inspect the group and assignment data available to the configured Graph identity. Keep the role assigned to a deliberately small, approved group, review membership regularly, and treat the Graph permissions and returned data as high-value.
Collecting heterogeneous resources
The requested policy families are collected concurrently. Each task adds normalized results to a thread-safe ConcurrentBag<IntuneAssignment>; substituting a regular List<T> would make those parallel writes unsafe.
Most categories follow the same broad pattern:
- Enumerate the relevant catalog, following Graph pagination.
- Retrieve assignment collections for those resources.
- Resolve group targets and assignment intent.
- Map resource-specific fields into the shared assignment model.
- Sort the final result for display.
The repository uses the Microsoft Graph SDK for group search and a dedicated GraphBeta HTTP client for raw beta endpoint calls. The service covers multiple resources including device configurations, Settings Catalog, administrative templates, compliance policies, mobile apps, app configurations, endpoint security intents, scripts, Autopilot profiles, enrollment configurations, and Cloud PC resources.
Some families need their own treatment. App Protection checks @odata.type to choose a resource-specific assignments path, while endpoint security must cover both Settings Catalog configurations and template-style intents. A unified screen has not made these differences disappear. Someone still has to encode them in each collector.
Code excerpt: batch work without corrupting concurrent results
The service starts category collectors concurrently. Since each collector adds to the shared result, the repository uses ConcurrentBag<IntuneAssignment> rather than a regular List<T>, then waits for the collectors before sorting the completed result. This abbreviated excerpt is from GetAssignmentsForGroupAsync in IntuneService.cs:
var assignments = new System.Collections.Concurrent.ConcurrentBag<IntuneAssignment>(); var tasks = new List<Task>(); if (filterType is null or PolicyType.Configuration) { tasks.Add(GetDeviceConfigurationsAsync(groupId, assignments)); tasks.Add(GetSettingsCatalogAsync(groupId, assignments)); } if (filterType is null or PolicyType.Compliance) tasks.Add(GetCompliancePoliciesAsync(groupId, assignments)); if (filterType is null or PolicyType.Application) tasks.Add(GetMobileAppsAsync(groupId, assignments)); await Task.WhenAll(tasks); return assignments .OrderBy(a => a.PolicyType) .ThenBy(a => a.PolicyName) .ToList();
The full method schedules the remaining requested categories similarly and logs collection errors. This shortened version shows why the shared result needs to tolerate parallel Add calls.
Keeping a many-endpoint view responsive
A single group selection can fan out into many catalog and assignment requests. Repeating all of them for each operator would soon make the simple screen feel less simple. The repository uses several controls:
- Tenant-wide in-memory response cache. A singleton
GraphResponseCacheshares results across user circuits. Catalogs, assignments, and group names use configurable time-to-live values. - Per-URL request coalescing. Concurrent requests for the same URL wait on one lock, so overlapping users do not all fetch an identical uncached response at once.
- Concurrency limiting and retries. A semaphore caps in-flight Graph calls; the service retries transient throttling responses and respects
Retry-After. - Negative caching. Responses that represent missing or forbidden resources are cached briefly to avoid repeatedly hitting endpoints that return 404 or 403.
- Bounded memory. A dedicated memory cache applies a configurable size limit rather than allowing Graph response data to grow without limit.
- Optional warmup and Graph batching. When enabled, a hosted service pre-fetches catalogs and assignment collections. Graph
$batchcan combine up to 20 GET operations, reducing the number of HTTP round trips during warmup. - Explicit refresh. The UI can bypass cached assignment data when an administrator needs a fresh collection.
You gain responsiveness and fewer repeated calls, at the cost of some freshness. Tune cache lifetime and warmup for tenant size, Graph throttling, host memory and how current the operator's answer needs to be. Warmup is disabled by default on premises. I would measure the normal workload before enabling more background collection.
Code excerpt: respect Graph throttling
The cache wrapper retries throttling and temporary service-unavailable responses. It prefers Graph's Retry-After value, with an exponential delay as a fallback, instead of immediately retrying in a tight loop. This abbreviated excerpt is from FetchWithRetryAsync in GraphResponseCache.cs:
if (response.StatusCode == System.Net.HttpStatusCode.TooManyRequests || (int)response.StatusCode == 503) { if (attempt < maxAttempts) { retryDelay = response.Headers.RetryAfter?.Delta ?? TimeSpan.FromSeconds(Math.Pow(2, attempt)); _logger.LogInformation( "Throttled ({Status}), retry in {Delay}s: {Url}", (int)response.StatusCode, retryDelay.Value.TotalSeconds, url); } }
Retries stop at maxAttempts, and other non-success responses have separate handling. Backoff avoids turning temporary throttling into a burst of even more requests. Graph limits still apply, and requests can still fail.
Deployment choices change the credential model
Where you host the application changes how it gets its Graph credential:
- Azure: use a system-assigned Managed Identity for Graph and grant it the required application permissions. This avoids a long-lived Graph client secret in application configuration.
- On-premises: there is no Managed Identity. Use a dedicated Graph app registration with a certificate where possible; the repository supports loading a certificate from the Windows certificate store, a PFX file, or base64 configuration. A client secret is also supported but is explicitly the less secure option.
On premises, I'd prefer a certificate in the Windows certificate store, with private-key access limited to the application-pool identity. Make the key non-exportable where supported. A PFX file or base64 certificate remains sensitive credential material: keep it out of source control and appsettings.json, restrict storage and configuration access, and plan renewal and rotation. Do not forget why there are two registrations. One signs users in; the other reads Graph. A combined registration is supported for labs or small deployments, but the repository recommends separate production registrations to limit the damage from an exposed credential.
Checks I'd make before rollout
First, be clear about the answer the portal gives. It aggregates assignments without proving device receipt, evaluation or application. You start from a group, then can drill into a found policy's targets, including policies found through a virtual scope. That is implemented functionality, but neither an independent policy-search workflow nor transitive-group analysis. Part 1 shows the detail view's separate targeting-mode, app-intent and filter fields.
Parts of the Intune coverage call Graph beta endpoints. Check behavior against current Microsoft documentation, test upgrades and watch Graph errors; beta contracts can change. A 403 may mean missing permission or an unavailable tenant feature. Looking at the viewer does not replace reviewing those permissions.
Before production, check the narrowly scoped app role assignments, Graph application permissions and consent, credential storage, TLS, redirect URIs, cache settings and the categories this tenant uses. Start with a small approved group of operators authorized for tenant-wide assignment visibility. Compare representative results with the admin center before relying on the new view.
The source, screenshots, configuration reference, and deployment guides are in the IntuneAssignmentViewer repository. The repository is MIT-licensed and credits the IntuneAssignmentChecker PowerShell module as inspiration for Graph endpoint coverage.