Series · Intune Assignment Viewer: from feature gap to implementation · Part 1 of 2
Microsoft Intune · Entra ID · Microsoft Graph · Blazor · Assignment Visibility
Intune Assignment Viewer: see a group's assignments
Find a group's Intune assignments on one screen, inspect policy targets and understand what the results can tell you about device targeting.
- Drafted
- Published
- Updated
- Reading time
- 13 min
- Author
- Roberto Gramellini
Source: Intune Assignment Viewer
- Intune assignments
- Intune assignment viewer
- Entra ID group
- Intune feature gap
- Microsoft Graph
- Intune policies
Intune Assignment Viewer: see a group's assignments
“This device should have received the policy.” Before chasing the device, I'd ask a smaller question: what is this group actually assigned? It ought to be quick to answer.
In Intune, that question can send you through several areas of the admin center. Configuration profiles, compliance policies, applications, scripts, endpoint security and enrollment settings do not appear in one consolidated group view. You collect the pieces and do the join in your head. By then the ticket has company: a small collection of browser tabs.
Intune Assignment Viewer removes much of that switching. Select an Entra ID group and the portal collects the Intune assignments targeting it onto one screen.
Start from the group
The assignment data is already there, in Microsoft Graph and the admin center. It is organized by policy resource and product area, though. An administrator asking about one group has to visit several blades and keep track of targets, inclusions, exclusions and assignment intent.
During a deployment check, missing one of those areas can change the answer. Finding the expected configuration profile is useful, but applications, compliance, endpoint security, scripts and provisioning assignments may matter just as much.
Here you select a target group and collect the policies assigned to it, rather than open a policy and inspect its targets. Keep that purpose in mind: this is for visibility and triage. Authoring policies and the wider administration work still belong in the Intune admin center.
Follow one deployment question through the screen
Take a fictional Windows Pilot group. It should receive a configuration profile, a required application and a remediation script. We want to inspect all three. We do not yet know whether any device has installed the application or evaluated either policy.
The application uses .NET 10 and Blazor Server: the browser provides the interactive view, while server-side services query Graph and return a common assignment model. A selection moves through group search, category collection and target matching before becoming normalized rows. Policy-target drill-down is available afterwards.
| What the operator does | What the application actually does |
|---|---|
| Types a group-name prefix | Searches directory groups after a short pause |
| Selects a group | Collects policy families and matches their assignment targets |
| Chooses a category or OS chip | Filters the rows already collected for that selection |
| Opens a policy's details | Retrieves that policy's targets, including other groups and broad scopes |
| Selects Refresh | Invalidates cached assignment responses and collects again |
Notice which actions actually need Graph. Changing from cards to a table, or narrowing the visible rows, can reuse the dataset already collected.
Search for a target, not for a policy title
Search begins at three characters, with a 400 ms debounce. The handler stops and disposes the previous timer, so every keystroke does not schedule its own directory search:
private async Task OnSearchInput() { debounceTimer?.Stop(); debounceTimer?.Dispose(); if (string.IsNullOrWhiteSpace(searchTerm) || searchTerm.Length < 3) { groups.Clear(); return; } debounceTimer = new System.Timers.Timer(400) { AutoReset = false }; debounceTimer.Elapsed += async (s, e) => { await InvokeAsync(async () => { await SearchGroups(); StateHasChanged(); }); }; debounceTimer.Start(); }
Source: search-input handler. Complete method; indentation normalized.
The directory service uses startsWith(displayName, ...), requests the group's ID, display name and description, and limits the response to 20 groups. This is prefix search, not arbitrary substring search or an exhaustive directory export. For the example group, begin with Windows, not a word that only appears in the middle of its name.
Debouncing saves work, but cannot cancel a request already running. Search exceptions are logged and the service returns the list it collected, which makes an empty dropdown ambiguous. I'd check the prefix, access and server logs before concluding that a group does not exist.
The All Users and All Devices shortcuts use internal sentinel values instead of real group IDs. They select assignments whose target types represent those broad scopes. Selecting an ordinary group does not automatically union those broad assignments into the group result, and selecting a virtual target does not enumerate its members.
Assemble the categories behind the single screen
Behind that one screen there are quite a few calls. The service enumerates resource catalogs, checks their assignment collections and adds matches to a thread-safe result bag. Category collectors run concurrently. Once they finish, rows are sorted by policy type and name.
The collectors cover device configuration, Settings Catalog, Administrative Templates, compliance, applications, App Protection, App Configuration, Endpoint Security, PowerShell and shell scripts, remediations, Autopilot profiles, Enrollment Status Page, and Cloud PC provisioning and user settings. Coverage depends on the Graph identity's permissions and the resources available in the tenant.
For catalog collection and policy drill-down, pagination is explicit:
private async IAsyncEnumerable<JsonElement> EnumerateBetaAsync(string initialUrl) { var nextUrl = initialUrl; // First page = catalog TTL; subsequent pages share same TTL var ttl = initialUrl.Contains("/assignments") ? AssignmentsTtl : CatalogTtl; while (!string.IsNullOrEmpty(nextUrl)) { var root = await _graphCache.GetAsync(nextUrl, ttl); if (root == null) yield break; if (root.Value.TryGetProperty("value", out var arr)) { foreach (var item in arr.EnumerateArray()) yield return item.Clone(); } nextUrl = root.Value.TryGetProperty("@odata.nextLink", out var nl) ? nl.GetString() : null; } }
Source: paged enumeration helper. Complete method; indentation normalized.
Give this helper a Graph collection URL and it yields an asynchronous sequence of JSON records. @odata.nextLink is essential once policies or targets exceed one page. Assignment URLs and catalog URLs get different cache lifetimes. If a response is missing, enumeration stops; that does not establish that the unvisited pages are empty.
Turn a Settings Catalog object into a useful row
Endpoint responses disagree on how to describe names, platforms and policy families. The UI needs one usable record: policy ID/name, category, subtype, platform, description, assignment intent and a URL for detail retrieval. Normalization supplies it.
The Settings Catalog collector is a useful complete example:
private async Task GetSettingsCatalogAsync(string groupId, System.Collections.Concurrent.ConcurrentBag<IntuneAssignment> assignments) { await foreach (var policy in EnumerateBetaAsync( "https://graph.microsoft.com/beta/deviceManagement/configurationPolicies?$top=100")) { var pid = GetStr(policy, "id"); var platforms = GetStr(policy, "platforms"); // Determine if this policy belongs to Endpoint Security string? esFamily = null; if (policy.TryGetProperty("templateReference", out var tr) && tr.ValueKind == JsonValueKind.Object) { var family = GetStr(tr, "templateFamily"); if (!string.IsNullOrEmpty(family) && EndpointSecurityFamilies.TryGetValue(family, out var friendly)) esFamily = friendly; } var assignUrl = $"https://graph.microsoft.com/beta/deviceManagement/configurationPolicies('{pid}')/assignments"; var intent = await FindGroupAssignmentAsync(assignUrl, groupId); if (intent == null) continue; assignments.Add(new IntuneAssignment { PolicyId = pid, PolicyName = GetStr(policy, "name"), PolicyType = esFamily != null ? PolicyType.EndpointSecurity : PolicyType.Configuration, PolicySubType = esFamily ?? "Settings Catalog", Platform = PlatformFromString(platforms), Description = GetStr(policy, "description"), AssignmentIntent = intent, AssignmentsUrl = assignUrl }); } }
Source: Settings Catalog collector. Complete method; indentation normalized.
I would pay particular attention to classification here. A configuration-policy resource can actually represent Endpoint Security. Checking the template family and mapping recognized values to a friendly subtype avoids putting every resource from that endpoint in the configuration category.
A plausible policy name is not enough to add a row. The policy needs a matching target, however relevant its name sounds to the pilot.
Match the target and understand what the intent means
The central matcher handles ordinary groups and the two virtual scopes. It reads the assignment target's @odata.type, compares a real groupId when appropriate, and prefers an explicit application intent over its Include/Exclude fallback:
private async Task<string?> FindGroupAssignmentAsync(string assignmentsUrl, string groupId) { var root = await GetBetaAsync(assignmentsUrl); if (root is null || !root.Value.TryGetProperty("value", out var arr)) return null; foreach (var a in arr.EnumerateArray()) { if (!a.TryGetProperty("target", out var target)) continue; var odataType = target.TryGetProperty("@odata.type", out var ot) ? ot.GetString() ?? "" : ""; bool matched; bool isExclusion = false; if (groupId == VirtualAllUsersId) { matched = odataType.Contains("allLicensedUsersAssignmentTarget", StringComparison.OrdinalIgnoreCase); } else if (groupId == VirtualAllDevicesId) { matched = odataType.Contains("allDevicesAssignmentTarget", StringComparison.OrdinalIgnoreCase); } else { if (!target.TryGetProperty("groupId", out var gId)) continue; if (!string.Equals(gId.GetString(), groupId, StringComparison.OrdinalIgnoreCase)) continue; matched = true; isExclusion = odataType.Contains("exclusion", StringComparison.OrdinalIgnoreCase); } if (!matched) continue; // Check for app intent (required/available/uninstall) on the assignment itself if (a.TryGetProperty("intent", out var intentEl) && intentEl.ValueKind == JsonValueKind.String) { var intentVal = intentEl.GetString(); if (!string.IsNullOrEmpty(intentVal)) return char.ToUpper(intentVal[0]) + intentVal[1..]; } return isExclusion ? "Exclude" : "Include"; } return null; }
Source: group-target matching method. Complete method; indentation normalized.
Keep Include/Exclude and Required/Available/Uninstall separate when reading the result. The former is targeting mode; the latter is application intent. The overview has one display value, allowing explicit app intent to take precedence over mode. Open policy details when you need to see both independently.
The method also returns the first matching assignment in the response, not every matching target. Unlike the paged helper above, it reads one assignment response and does not follow a next-page link itself. It should not be read as a full reconciliation of multiple matching assignments, filters, group memberships or assignment precedence.
So “Required” in our pilot means a matching app assignment was found with that intent. Device eligibility, download and completed installation remain unproven.
Filter the working set without confusing it with applicability
Counts come from the collected rows. Tabs, platform chips and charts help you inspect that set; none queries each device to evaluate hardware or OS properties.
The filtering method operates on the existing assignments collection:
private void ApplyFilters() { IEnumerable<IntuneAssignment> q = assignments; if (selectedFilter != null) q = q.Where(a => a.PolicyType == selectedFilter); if (!string.IsNullOrEmpty(selectedPlatform)) { q = (selectedPlatform == "Cross-platform") ? q.Where(a => string.IsNullOrEmpty(a.Platform)) : q.Where(a => a.Platform == selectedPlatform); } filteredAssignments = q.ToList(); }
Source: local view filtering. Complete method; indentation normalized.
An empty platform field gets the label “Cross-platform.” Read that carefully: it does not prove applicability to every operating system. The OS chip is also a view filter, not an Intune assignment filter. “Showing N of M” counts rows, not successfully targeted or compliant devices.
Open a policy to inspect the other targets
Once a group or virtual scope leads you to a policy, details let you work in the other direction. Open the policy to inspect its targets, including other groups, All Users and All Devices. That capability is implemented already, though it does not provide a separate tenant-wide policy-search workflow.
The UI starts that drill-down using the assignment URL carried by the normalized row:
private async Task OpenDetails(IntuneAssignment a) { detailsAssignment = a; detailsList = new(); isLoadingDetails = true; StateHasChanged(); try { detailsList = await IntuneService.GetPolicyAssignmentsAsync(a.AssignmentsUrl); } finally { isLoadingDetails = false; StateHasChanged(); } }
Source: policy-details handler. Complete method; indentation normalized.
The detail collector validates the URL against the application's permitted Graph assignment-path pattern before fetching it. It uses paged enumeration, keeps exclusion mode and app intent in separate fields, and retains assignment-filter IDs and types. Directory display names are resolved through cached lookups and Graph batches rather than one blocking lookup per table cell.
The dialog displays Target, Mode, Intent and Filter columns. Its Filter column shows the filter type, not the full filter definition or an evaluation result. The service model retains the filter ID, but the displayed badge does not explain the underlying rule. An unresolved directory name can also appear as an unknown/deleted fallback; that label alone does not prove deletion.
For our pilot, this is where a broad app assignment might turn out to have a group exclusion, or a matching target might reveal an assignment filter. Either observation gives you a concrete next check in Intune. The viewer still does not calculate the final device-level outcome.
Freshness, access and incomplete results are part of the answer
The Refresh button invalidates cached assignment responses before calling the collection service again. Catalog and group-name caches are separate, so it is not a promise to discard every cached value or to observe a just-created policy immediately. The cache and throttling design is covered in part 2.
An empty or unusually small result needs investigation. The category coordinator catches and logs collection errors while returning records from the other tasks, so a failed category can leave a partial view. Missing permissions, unavailable tenant features, Graph beta changes, cached errors and paging boundaries can all affect the result. Clean formatting does not certify completeness.
Before relying on the screen, configure administrator sign-in and the portal's application role, then grant only the Graph application permissions required for the supported resource families. The Graph reads use the application's identity, not the signed-in operator's delegated Intune permissions. Restrict portal access to operators authorized to see that tenant-wide data. Use Managed Identity for the supported Azure deployment or protect the separate on-premises Graph credential; keep credentials out of source control and the browser.
My starting acceptance check would compare a known configuration assignment, an app intent and a script assignment with Intune. Add an exclusion, a broad-scope assignment and a policy carrying an assignment filter. Record what you queried, refresh as needed and inspect server logs for missing categories. An assignment listing cannot establish installation, compliance, effective nested-group targeting or successful policy application.
Explore the implementation
Start with the IntuneAssignmentViewer repository, then read the architecture and operations companion for identity separation, cache behavior, batching and deployment trade-offs. The source also credits the IntuneAssignmentChecker PowerShell module as inspiration for Graph endpoint coverage.
All six excerpts reproduce complete methods from the linked source, with indentation normalized; they depend on the surrounding application's services, helpers, models and imports. They are explanatory excerpts, not standalone programs or a claim that every implementation choice is an operational guarantee.
License notice for reproduced code
The source is MIT-licensed. The explanation is original; the code excerpts retain the source's notice:
Copyright (c) 2026 Roberto Grameno
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.