--- phase: 07-user-access-audit plan: 03 type: execute wave: 2 depends_on: ["07-01"] files_modified: - SharepointToolbox/Services/GraphUserSearchService.cs autonomous: true requirements: - UACC-01 must_haves: truths: - "GraphUserSearchService queries Microsoft Graph /users endpoint with $filter for displayName/mail startsWith" - "Service returns GraphUserResult records with DisplayName, UPN, and Mail" - "Service handles empty queries and returns empty list" - "Service uses existing GraphClientFactory for authentication" artifacts: - path: "SharepointToolbox/Services/GraphUserSearchService.cs" provides: "Implementation of IGraphUserSearchService for people-picker autocomplete" contains: "class GraphUserSearchService" key_links: - from: "SharepointToolbox/Services/GraphUserSearchService.cs" to: "SharepointToolbox/Infrastructure/Auth/GraphClientFactory.cs" via: "Constructor injection, CreateClientAsync call" pattern: "CreateClientAsync" --- Implement GraphUserSearchService that queries Microsoft Graph API to search tenant users by name or email. Powers the people-picker autocomplete in the audit tab. Purpose: Enables administrators to find and select tenant users by typing partial names/emails, rather than typing exact login names manually. Output: GraphUserSearchService.cs @C:/Users/dev/.claude/get-shit-done/workflows/execute-plan.md @C:/Users/dev/.claude/get-shit-done/templates/summary.md @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/phases/07-user-access-audit/07-CONTEXT.md @.planning/phases/07-user-access-audit/07-01-SUMMARY.md From SharepointToolbox/Services/IGraphUserSearchService.cs: ```csharp public interface IGraphUserSearchService { Task> SearchUsersAsync( string clientId, string query, int maxResults = 10, CancellationToken ct = default); } public record GraphUserResult(string DisplayName, string UserPrincipalName, string? Mail); ``` From SharepointToolbox/Infrastructure/Auth/GraphClientFactory.cs: ```csharp public class GraphClientFactory { public async Task CreateClientAsync(string clientId, CancellationToken ct); } ``` Task 1: Implement GraphUserSearchService SharepointToolbox/Services/GraphUserSearchService.cs Create `SharepointToolbox/Services/GraphUserSearchService.cs`: ```csharp using SharepointToolbox.Infrastructure.Auth; namespace SharepointToolbox.Services; /// /// Searches tenant users via Microsoft Graph API. /// Used by the people-picker autocomplete in the User Access Audit tab. /// public class GraphUserSearchService : IGraphUserSearchService { private readonly GraphClientFactory _graphClientFactory; public GraphUserSearchService(GraphClientFactory graphClientFactory) { _graphClientFactory = graphClientFactory; } public async Task> SearchUsersAsync( string clientId, string query, int maxResults = 10, CancellationToken ct = default) { if (string.IsNullOrWhiteSpace(query) || query.Length < 2) return Array.Empty(); var graphClient = await _graphClientFactory.CreateClientAsync(clientId, ct); // Use $filter with startsWith on displayName and mail. // Graph API requires ConsistencyLevel=eventual for advanced queries. var escapedQuery = query.Replace("'", "''"); var response = await graphClient.Users.GetAsync(config => { config.QueryParameters.Filter = $"startsWith(displayName,'{escapedQuery}') or startsWith(mail,'{escapedQuery}') or startsWith(userPrincipalName,'{escapedQuery}')"; config.QueryParameters.Select = new[] { "displayName", "userPrincipalName", "mail" }; config.QueryParameters.Top = maxResults; config.QueryParameters.Orderby = new[] { "displayName" }; config.Headers.Add("ConsistencyLevel", "eventual"); config.QueryParameters.Count = true; }, ct); if (response?.Value is null) return Array.Empty(); return response.Value .Select(u => new GraphUserResult( DisplayName: u.DisplayName ?? u.UserPrincipalName ?? "Unknown", UserPrincipalName: u.UserPrincipalName ?? string.Empty, Mail: u.Mail)) .ToList(); } } ``` Design notes: - Minimum 2 characters before searching (prevents overly broad queries) - Uses startsWith filter on displayName, mail, and UPN for broad matching - Single quotes in query are escaped to prevent OData injection - ConsistencyLevel=eventual header required for startsWith filter on directory objects - Count=true is required alongside ConsistencyLevel=eventual - Returns max 10 results by default (people picker dropdown) - Uses existing GraphClientFactory which handles MSAL token acquisition cd "C:\Users\dev\Documents\projets\Sharepoint" && dotnet build SharepointToolbox/SharepointToolbox.csproj --no-incremental 2>&1 | tail -5 GraphUserSearchService.cs compiles, implements IGraphUserSearchService, uses GraphClientFactory for auth, queries Graph /users with startsWith filter, returns GraphUserResult list. - `dotnet build SharepointToolbox/SharepointToolbox.csproj` succeeds with 0 errors - GraphUserSearchService implements IGraphUserSearchService - Uses GraphClientFactory.CreateClientAsync (not raw HTTP) - Handles empty/short queries gracefully (returns empty list) - Filter uses startsWith on displayName, mail, and UPN The Graph people search service is implemented: given a partial name/email query, it returns matching tenant users via Microsoft Graph API. Ready for ViewModel consumption in 07-04 (people picker debounced autocomplete). After completion, create `.planning/phases/07-user-access-audit/07-03-SUMMARY.md`