Files
Sharepoint-Toolbox/SharepointToolbox/Services/GraphUserDirectoryService.cs
Dev 9a98371edd feat(13-01): extend GraphDirectoryUser with UserType and add includeGuests parameter to directory service
- Add string? UserType as last positional parameter to GraphDirectoryUser record
- Add bool includeGuests = false parameter to IGraphUserDirectoryService.GetUsersAsync
- Branch Graph filter: members-only (default) vs all users when includeGuests=true
- Add userType to Graph Select array for MapUser population
- Update MapUser to include UserType from Graph User object
- Add MapUser_PopulatesUserType and MapUser_NullUserType tests

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 16:01:46 +02:00

82 lines
2.9 KiB
C#

using Microsoft.Graph;
using Microsoft.Graph.Models;
using SharepointToolbox.Core.Models;
using AppGraphClientFactory = SharepointToolbox.Infrastructure.Auth.GraphClientFactory;
namespace SharepointToolbox.Services;
/// <summary>
/// Enumerates all enabled member users from a tenant via Microsoft Graph,
/// using PageIterator for transparent multi-page iteration.
/// Used by Phase 13's User Directory ViewModel.
/// </summary>
public class GraphUserDirectoryService : IGraphUserDirectoryService
{
private readonly AppGraphClientFactory _graphClientFactory;
public GraphUserDirectoryService(AppGraphClientFactory graphClientFactory)
{
_graphClientFactory = graphClientFactory;
}
/// <inheritdoc />
public async Task<IReadOnlyList<GraphDirectoryUser>> GetUsersAsync(
string clientId,
bool includeGuests = false,
IProgress<int>? progress = null,
CancellationToken ct = default)
{
var graphClient = await _graphClientFactory.CreateClientAsync(clientId, ct);
var response = await graphClient.Users.GetAsync(config =>
{
config.QueryParameters.Filter = includeGuests
? "accountEnabled eq true"
: "accountEnabled eq true and userType eq 'Member'";
config.QueryParameters.Select = new[]
{
"displayName", "userPrincipalName", "mail", "department", "jobTitle", "userType"
};
config.QueryParameters.Top = 999;
// No ConsistencyLevel header: standard equality filter does not require eventual consistency
}, ct);
if (response is null)
return Array.Empty<GraphDirectoryUser>();
var results = new List<GraphDirectoryUser>();
var pageIterator = PageIterator<User, UserCollectionResponse>.CreatePageIterator(
graphClient,
response,
user =>
{
// Honour cancellation inside the callback — returning false stops iteration
if (ct.IsCancellationRequested)
return false;
results.Add(MapUser(user));
progress?.Report(results.Count);
return true;
});
await pageIterator.IterateAsync(ct);
return results;
}
/// <summary>
/// Maps a Graph SDK <see cref="User"/> object to a <see cref="GraphDirectoryUser"/> record.
/// Extracted as an internal static method to allow direct unit-test coverage of mapping logic
/// without requiring a live Graph endpoint.
/// </summary>
internal static GraphDirectoryUser MapUser(User user) =>
new(
DisplayName: user.DisplayName ?? user.UserPrincipalName ?? string.Empty,
UserPrincipalName: user.UserPrincipalName ?? string.Empty,
Mail: user.Mail,
Department: user.Department,
JobTitle: user.JobTitle,
UserType: user.UserType);
}