Files
Sharepoint-Toolbox/.planning/phases/07-user-access-audit/07-03-PLAN.md
Dev 19e4c3852d docs(07): create phase plan - 8 plans across 5 waves
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 12:32:39 +02:00

6.5 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
07-user-access-audit 03 execute 2
07-01
SharepointToolbox/Services/GraphUserSearchService.cs
true
UACC-01
truths artifacts key_links
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
path provides contains
SharepointToolbox/Services/GraphUserSearchService.cs Implementation of IGraphUserSearchService for people-picker autocomplete class GraphUserSearchService
from to via pattern
SharepointToolbox/Services/GraphUserSearchService.cs SharepointToolbox/Infrastructure/Auth/GraphClientFactory.cs Constructor injection, CreateClientAsync call 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

<execution_context> @C:/Users/dev/.claude/get-shit-done/workflows/execute-plan.md @C:/Users/dev/.claude/get-shit-done/templates/summary.md </execution_context>

@.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);


<!-- Existing auth infrastructure -->
From SharepointToolbox/Infrastructure/Auth/GraphClientFactory.cs:
```csharp
public class GraphClientFactory
{
    public async Task<GraphServiceClient> 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;

/// <summary>
/// Searches tenant users via Microsoft Graph API.
/// Used by the people-picker autocomplete in the User Access Audit tab.
/// </summary>
public class GraphUserSearchService : IGraphUserSearchService
{
    private readonly GraphClientFactory _graphClientFactory;

    public GraphUserSearchService(GraphClientFactory graphClientFactory)
    {
        _graphClientFactory = graphClientFactory;
    }

    public async Task<IReadOnlyList<GraphUserResult>> SearchUsersAsync(
        string clientId,
        string query,
        int maxResults = 10,
        CancellationToken ct = default)
    {
        if (string.IsNullOrWhiteSpace(query) || query.Length < 2)
            return Array.Empty<GraphUserResult>();

        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<GraphUserResult>();

        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

<success_criteria> 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). </success_criteria>

After completion, create `.planning/phases/07-user-access-audit/07-03-SUMMARY.md`