Files
Sharepoint-Toolbox/.planning/phases/10-branding-data-foundation/10-RESEARCH.md
2026-04-08 11:43:07 +02:00

27 KiB
Raw Blame History

Phase 10: Branding Data Foundation - Research

Researched: 2026-04-08 Domain: C# WPF / .NET 10 — JSON persistence, image validation, Microsoft Graph SDK pagination Confidence: HIGH

Summary

Phase 10 is a pure infrastructure phase: no UI, no new NuGet packages. It introduces three new models (LogoData, BrandingSettings, plus extends TenantProfile), two repositories (BrandingRepository mirroring SettingsRepository), two services (BrandingService for validation/compression, GraphUserDirectoryService for paginated Graph enumeration), and registration of those in App.xaml.cs. All work is additive — nothing in the existing stack is removed or renamed.

The central technical challenge splits into two independent tracks:

  1. Logo storage track: Image format detection from magic bytes, silent compression using System.Drawing.Common (available via WPF's PresentationCore/System.Drawing.Common BCL subset on net10.0-windows), base64 serialization in JSON.
  2. Graph directory track: PageIterator<User, UserCollectionResponse> from Microsoft.Graph 5.x following @odata.nextLink until exhausted, with CancellationToken threading throughout.

Both tracks fit the existing patterns precisely. The repository uses SemaphoreSlim(1,1) + write-then-move. The Graph service clones GraphUserSearchService structure while substituting PageIterator for a one-shot GetAsync. No configuration, no new packages, no breaking changes.

Primary recommendation: Implement in order — models first, then repository, then services, then DI registration, then update ProfileManagementViewModel.DeleteAsync warning message. Tests mirror the SettingsServiceTests and ProfileServiceTests patterns already present.


<user_constraints>

User Constraints (from CONTEXT.md)

Locked Decisions

Decision Value
Logo storage format Base64 strings in JSON (not file paths)
MSP logo location BrandingSettings.cs model → branding.json
Client logo location On TenantProfile model (per-tenant)
File path after import Discarded — only base64 persists
SVG support Rejected (XSS risk) — PNG/JPG only
User directory service New GraphUserDirectoryService, separate from GraphUserSearchService
Directory auto-load No — explicit "Load Directory" button required
New NuGet packages None — existing stack covers everything
Export service signature Optional ReportBranding? branding = null parameter on existing export methods

Claude's Discretion

  • No discretion areas defined for Phase 10 — all decisions locked.

Deferred Ideas (OUT OF SCOPE)

  • Logo preview in Settings UI (Phase 12)
  • Auto-pull client logo from Entra branding API (Phase 11/12)
  • Report header layout with logos side-by-side (Phase 11)
  • "Load Directory" button placement decision (Phase 14)
  • Session-scoped directory cache (UDIR-F01, deferred) </user_constraints>

<phase_requirements>

Phase Requirements

ID Description Research Support
BRAND-01 User can import an MSP logo in application settings (global, persisted across sessions) BrandingSettings model + BrandingRepository (mirrors SettingsRepository) + BrandingService.ImportLogoAsync
BRAND-03 User can import a client logo per tenant profile LogoData? ClientLogo property on TenantProfile + ProfileRepository already handles serialization; BrandingService.ImportLogoAsync reused
BRAND-06 Logo import validates format (PNG/JPG) and enforces 512 KB size limit Magic byte detection (PNG: 89 50 4E 47, JPEG: FF D8 FF) + auto-compress via System.Drawing/BitmapEncoder if > 512 KB
</phase_requirements>

Standard Stack

Core (all already present — zero new installs)

Library Version Purpose Why Standard
System.Text.Json BCL (net10.0) JSON serialization of models Already used in all repositories
System.Drawing.Common BCL (net10.0-windows) Image load, resize, re-encode for compression Available on Windows via UseWPF=true; no extra package
Microsoft.Graph 5.74.0 (already in csproj) Graph SDK for user enumeration Already used by GraphUserSearchService
Microsoft.Identity.Client 4.83.3 (already in csproj) Token acquisition via GraphClientFactory Already used
CommunityToolkit.Mvvm 8.4.2 (already in csproj) [ObservableProperty] for ViewModels — not used in Phase 10 directly, but referenced by ProfileManagementViewModel Already used

No New Packages

All capabilities are covered by the existing stack. Confirmed in CONTEXT.md locked decisions and csproj inspection.

Architecture Patterns

SharepointToolbox/
├── Core/
│   └── Models/
│       ├── LogoData.cs               -- record LogoData(string Base64, string MimeType)
│       └── BrandingSettings.cs       -- class BrandingSettings { LogoData? MspLogo; }
├── Infrastructure/
│   └── Persistence/
│       └── BrandingRepository.cs     -- clone of SettingsRepository<BrandingSettings>
├── Services/
│   ├── IBrandingService.cs           -- ImportLogoAsync, ClearLogoAsync
│   ├── BrandingService.cs            -- validates magic bytes, compresses, returns LogoData
│   ├── IGraphUserDirectoryService.cs -- GetUsersAsync with PageIterator
│   └── GraphUserDirectoryService.cs  -- PageIterator pagination

SharepointToolbox.Tests/
└── Services/
    ├── BrandingServiceTests.cs       -- magic bytes, compression, rejection
    └── GraphUserDirectoryServiceTests.cs  -- pagination (mocked PageIterator or direct list)

Pattern 1: Repository (write-then-move with SemaphoreSlim)

Exact clone of SettingsRepository with BrandingSettings substituted for AppSettings. No deviations.

// Source: SharepointToolbox/Infrastructure/Persistence/SettingsRepository.cs (existing)
public class BrandingRepository
{
    private readonly string _filePath;
    private readonly SemaphoreSlim _writeLock = new(1, 1);

    public async Task<BrandingSettings> LoadAsync()
    {
        if (!File.Exists(_filePath))
            return new BrandingSettings();
        // ... File.ReadAllTextAsync + JsonSerializer.Deserialize<BrandingSettings> ...
    }

    public async Task SaveAsync(BrandingSettings settings)
    {
        await _writeLock.WaitAsync();
        try
        {
            var json = JsonSerializer.Serialize(settings,
                new JsonSerializerOptions { WriteIndented = true, PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
            var tmpPath = _filePath + ".tmp";
            // ... write to tmp, validate round-trip, File.Move(tmp, _filePath, overwrite: true) ...
        }
        finally { _writeLock.Release(); }
    }
}

Pattern 2: LogoData record — shared by MSP and client logos

// Source: CONTEXT.md §1 Logo Metadata Model
namespace SharepointToolbox.Core.Models;

public record LogoData(string Base64, string MimeType);
// MimeType is "image/png" or "image/jpeg" — determined at import time from magic bytes
// Usage in HTML: $"data:{MimeType};base64,{Base64}"

Pattern 3: BrandingSettings model

namespace SharepointToolbox.Core.Models;

public class BrandingSettings
{
    public LogoData? MspLogo { get; set; }
}

Pattern 4: TenantProfile extension

// Extend existing TenantProfile — additive, no breaking change
public class TenantProfile
{
    public string Name { get; set; } = string.Empty;
    public string TenantUrl { get; set; } = string.Empty;
    public string ClientId { get; set; } = string.Empty;
    public LogoData? ClientLogo { get; set; }   // NEW — nullable, ignored when null in JSON
}

ProfileRepository needs no code change — System.Text.Json serializes the new nullable property automatically. Existing profiles JSON without clientLogo deserializes with null (forward-compatible).

Pattern 5: Magic byte validation + compression in BrandingService

// Source: CONTEXT.md §2 Logo Validation & Compression
private static readonly byte[] PngSignature = { 0x89, 0x50, 0x4E, 0x47 };
private static readonly byte[] JpegSignature = { 0xFF, 0xD8, 0xFF };

private static string? DetectMimeType(byte[] header)
{
    if (header.Length >= 4 && header.Take(4).SequenceEqual(PngSignature)) return "image/png";
    if (header.Length >= 3 && header.Take(3).SequenceEqual(JpegSignature)) return "image/jpeg";
    return null;
}

public async Task<LogoData> ImportLogoAsync(string filePath)
{
    var bytes = await File.ReadAllBytesAsync(filePath);
    var mimeType = DetectMimeType(bytes)
        ?? throw new InvalidDataException("File format is not PNG or JPG. Only PNG and JPG are accepted.");

    if (bytes.Length > 512 * 1024)
        bytes = CompressToLimit(bytes, mimeType, maxBytes: 512 * 1024);

    return new LogoData(Convert.ToBase64String(bytes), mimeType);
}

For compression, use System.Drawing.Bitmap (available on net10.0-windows) to resize to max 300×300px and re-encode at reduced quality using System.Drawing.Imaging.ImageCodecInfo/EncoderParameters. Keep original format.

Pattern 6: GraphUserDirectoryService with PageIterator

Microsoft.Graph 5.x includes PageIterator<TEntity, TCollectionPage> in Microsoft.Graph.Core. Pattern from Graph SDK docs:

// Source: Microsoft.Graph 5.x SDK — PageIterator pattern
public async Task<IReadOnlyList<GraphDirectoryUser>> GetUsersAsync(
    string clientId,
    CancellationToken ct = default)
{
    var graphClient = await _graphClientFactory.CreateClientAsync(clientId, ct);
    var results = new List<GraphDirectoryUser>();

    var response = await graphClient.Users.GetAsync(config =>
    {
        config.QueryParameters.Filter = "accountEnabled eq true and userType eq 'Member'";
        config.QueryParameters.Select = new[] { "displayName", "userPrincipalName", "mail", "department", "jobTitle" };
        config.QueryParameters.Top = 999;
    }, ct);

    if (response is null) return results;

    var pageIterator = PageIterator<User, UserCollectionResponse>.CreatePageIterator(
        graphClient,
        response,
        user =>
        {
            results.Add(new GraphDirectoryUser(
                user.DisplayName ?? user.UserPrincipalName ?? string.Empty,
                user.UserPrincipalName ?? string.Empty,
                user.Mail,
                user.Department,
                user.JobTitle));
            return true; // continue iteration
        });

    await pageIterator.IterateAsync(ct);
    return results;
}

PageIterator requires Microsoft.Graph.Core which is a transitive dependency of Microsoft.Graph 5.x — already present.

No ConsistencyLevel: eventual needed for the $filter query with accountEnabled and userType — these are standard properties, not advanced queries requiring $count. (Unlike the search service which uses startsWith and requires ConsistencyLevel.)

Pattern 7: DI registration (App.xaml.cs)

// Phase 10: Branding Data Foundation
services.AddSingleton(_ => new BrandingRepository(Path.Combine(appData, "branding.json")));
services.AddSingleton<BrandingService>();
services.AddTransient<IGraphUserDirectoryService, GraphUserDirectoryService>();

BrandingRepository is Singleton (same rationale as ProfileRepository and SettingsRepository — single file, shared lock). BrandingService is Singleton (stateless after construction, depends on singleton repository). GraphUserDirectoryService is Transient (per-tenant call, stateless).

Pattern 8: ProfileManagementViewModel deletion message update

In ProfileManagementViewModel.DeleteAsync(), the existing confirmation flow has no dialog — it directly calls _profileService.DeleteProfileAsync. The update per CONTEXT.md is to augment the confirmation message (when that dialog exists) to mention logo removal. However, Phase 10 does not add a confirmation dialog — that is the caller's concern (View layer, Phase 12). The ViewModel update is to expose information about whether a profile has a logo, enabling Phase 12's View to conditionally show the warning.

// Add a computed property to support the deletion warning in Phase 12
// This is the minimal Phase 10 change:
// TenantProfile.ClientLogo != null → the confirmation dialog (Phase 12) reads this

The actual deletion behavior is unchanged: deleting the profile JSON entry automatically drops the embedded clientLogo field. No orphaned files exist.

Anti-Patterns to Avoid

  • Do not store the file path in JSON — only base64 + MIME type. File path is discarded immediately after reading bytes.
  • Do not use file extension for format detection — always read magic bytes from the byte array.
  • Do not use $search or $count on the directory queryPageIterator with $filter=accountEnabled eq true and userType eq 'Member' does not require ConsistencyLevel: eventual.
  • Do not create a new interface for BrandingRepositorySettingsRepository has no interface either; only services get interfaces.
  • Do not add [ObservableProperty] to LogoData — it is a plain record used in persistence layer; ViewModel bindings come in Phase 11-12.

Don't Hand-Roll

Problem Don't Build Use Instead Why
JSON pagination follow-up Manual @odata.nextLink string parsing loop PageIterator<User, UserCollectionResponse> SDK handles retry, null checks, async iteration natively
Image format detection File extension check Magic byte read on first 4 bytes Extensions are user-controlled and unreliable
Atomic file write Direct File.WriteAllText Write to .tmp, validate, File.Move(overwrite:true) Crash during write leaves corrupted JSON; pattern already proven in all repos
Concurrency guard lock(obj) SemaphoreSlim(1,1) Async-safe; lock cannot be awaited
Base64 encoding Manual byte-to-char loop Convert.ToBase64String(bytes) BCL, zero allocation path, no edge cases

Common Pitfalls

Pitfall 1: System.Drawing availability on net10.0-windows

What goes wrong: System.Drawing.Common is available on Windows (the project already targets net10.0-windows with UseWPF=true) but would throw PlatformNotSupportedException on Linux/macOS runtimes. Why it happens: .NET 6+ restricted System.Drawing.Common to Windows-only by default. How to avoid: This project is Windows-only (WinExe, UseWPF=true) so no risk. No guard needed. Warning signs: CI on Linux — not applicable here.

Pitfall 2: PageIterator.IterateAsync does not accept CancellationToken directly in Graph SDK 5.x

What goes wrong: PageIterator.IterateAsync() in Microsoft.Graph 5.x overloads — the token must be passed when calling CreatePageIterator, and the iteration callback must check cancellation manually or the token goes to IterateAsync(ct) if the overload exists. Why it happens: API surface changed between SDK versions. How to avoid: Check token inside the callback: if (ct.IsCancellationRequested) return false; stops iteration. Also pass ct to the initial GetAsync call. Warning signs: Long-running enumeration that ignores cancellation requests.

Pitfall 3: Deserialization of LogoData record with System.Text.Json

What goes wrong: C# records with positional constructors may not deserialize correctly with System.Text.Json unless the property names match constructor parameter names exactly (case-insensitive with PropertyNameCaseInsensitive = true) or a [JsonConstructor] attribute is present. Why it happens: Positional record constructor parameters are base64 and mimeType (camelCase) while JSON uses PropertyNamingPolicy.CamelCase. How to avoid: Use a class with { get; set; } properties OR add [JsonConstructor] to the positional record constructor. Simpler: make LogoData a class with init setters or a non-positional record with { get; init; } properties.

// SAFE version — class-style record with init setters:
public record LogoData
{
    public string Base64 { get; init; } = string.Empty;
    public string MimeType { get; init; } = string.Empty;
}

Pitfall 4: Large base64 string bloating profiles.json

What goes wrong: A 512 KB logo becomes ~682 KB of base64 text. Per-profile, this is manageable. However, ProfileRepository.LoadAsync loads ALL profiles at once — 20 tenants with logos = ~14 MB in memory per load. Why it happens: All profiles are stored in a single JSON array. How to avoid: Phase 10 does not address this (deferred); the 512 KB cap keeps it bounded. Document as known limitation. Warning signs: Not a Phase 10 concern — flag for future phases if profile count grows large.

Pitfall 5: File.Move with overwrite: true not available on all .NET versions

What goes wrong: File.Move(src, dst, overwrite: true) was added in .NET 3.0. On older frameworks this throws. Why it happens: Legacy API surface. How to avoid: Not applicable — project targets net10.0. Use freely.

Pitfall 6: Graph $filter without ConsistencyLevel on advanced queries

What goes wrong: The search service uses startsWith() which requires ConsistencyLevel: eventual + $count=true. If the directory service accidentally includes $count or $search, it needs the header too. Why it happens: Copy-paste from GraphUserSearchService without removing the ConsistencyLevel header. How to avoid: The directory filter accountEnabled eq true and userType eq 'Member' is a standard equality filter — does NOT require ConsistencyLevel: eventual. Do not copy the header from GraphUserSearchService.

Code Examples

Magic Byte Detection

// Source: CONTEXT.md §2 Logo Validation; confirmed against PNG/JPEG specs
private static readonly byte[] PngMagic  = { 0x89, 0x50, 0x4E, 0x47 };
private static readonly byte[] JpegMagic = { 0xFF, 0xD8, 0xFF };

private static string? DetectMimeType(ReadOnlySpan<byte> header)
{
    if (header.Length >= 4 && header[..4].SequenceEqual(PngMagic))  return "image/png";
    if (header.Length >= 3 && header[..3].SequenceEqual(JpegMagic)) return "image/jpeg";
    return null;
}

Compression via System.Drawing (net10.0-windows)

// Source: BCL System.Drawing.Common — Windows-only, safe here
private static byte[] CompressImage(byte[] original, string mimeType, int maxBytes)
{
    using var ms = new MemoryStream(original);
    using var bitmap = new System.Drawing.Bitmap(ms);

    // Scale down proportionally to max 300px
    int w = bitmap.Width, h = bitmap.Height;
    if (w > 300 || h > 300)
    {
        double scale = Math.Min(300.0 / w, 300.0 / h);
        w = (int)(w * scale);
        h = (int)(h * scale);
    }

    using var resized = new System.Drawing.Bitmap(bitmap, w, h);

    // Re-encode
    var codec = System.Drawing.Imaging.ImageCodecInfo.GetImageEncoders()
        .First(c => c.MimeType == mimeType);
    var encoderParams = new System.Drawing.Imaging.EncoderParameters(1);
    encoderParams.Param[0] = new System.Drawing.Imaging.EncoderParameter(
        System.Drawing.Imaging.Encoder.Quality, 75L);

    using var output = new MemoryStream();
    resized.Save(output, codec, encoderParams);
    return output.ToArray();
}

PageIterator pattern (Microsoft.Graph 5.x)

// Source: Microsoft.Graph 5.x SDK pattern; PageIterator<TEntity, TCollectionPage>
var pageIterator = PageIterator<User, UserCollectionResponse>.CreatePageIterator(
    graphClient,
    firstPage,
    user =>
    {
        if (ct.IsCancellationRequested) return false;
        results.Add(MapUser(user));
        return true;
    });

await pageIterator.IterateAsync(ct);

GraphDirectoryUser result record

// New record for Phase 10 — placed in Services/ or Core/Models/
public record GraphDirectoryUser(
    string DisplayName,
    string UserPrincipalName,
    string? Mail,
    string? Department,
    string? JobTitle);

JSON shape of branding.json

{
  "mspLogo": {
    "base64": "iVBORw0KGgo...",
    "mimeType": "image/png"
  }
}

JSON shape of profiles.json (after Phase 10)

{
  "profiles": [
    {
      "name": "Contoso",
      "tenantUrl": "https://contoso.sharepoint.com",
      "clientId": "...",
      "clientLogo": {
        "base64": "/9j/4AAQ...",
        "mimeType": "image/jpeg"
      }
    },
    {
      "name": "Fabrikam",
      "tenantUrl": "https://fabrikam.sharepoint.com",
      "clientId": "...",
      "clientLogo": null
    }
  ]
}

State of the Art

Old Approach Current Approach When Changed Impact
Manual @odata.nextLink loop PageIterator<T, TPage> Microsoft.Graph 5.x (current) Handles backoff, null-safety, async natively
System.Drawing everywhere System.Drawing Windows-only .NET 6 No impact here — Windows-only project
Class-based Graph response models Record/POCO Value collections Microsoft.Graph 5.x response.Value is List<User>?

Deprecated/outdated:

  • Microsoft.Graph.Beta namespace: not needed here — standard /v1.0/users endpoint sufficient
  • IAuthenticationProvider (old Graph SDK): replaced by BaseBearerTokenAuthenticationProvider — already correct in GraphClientFactory

Open Questions

  1. CancellationToken in PageIterator.IterateAsync — exact overload in Graph SDK 5.74.0

    • What we know: PageIterator exists in Microsoft.Graph.Core; IterateAsync exists. Token passing confirmed in SDK samples.
    • What's unclear: Whether IterateAsync(CancellationToken) overload exists in 5.74.0 or only the parameterless version.
    • Recommendation: Check when implementing. If parameterless only, use ct.IsCancellationRequested inside callback to return false and stop iteration. Either approach works correctly.
  2. $filter=accountEnabled eq true and userType eq 'Member' — verified against real tenant?

    • STATE.md flags this as a pending todo: "Confirm $filter=accountEnabled eq true and userType eq 'Member' behavior without ConsistencyLevel: eventual against a real tenant before Phase 13 planning."
    • Phase 10 implements the service; Phase 13 will exercise the filter in the ViewModel. The pending verification is appropriate for Phase 13.
    • Recommendation: Implement the filter as specified. Flag in GraphUserDirectoryService with a comment noting the pending verification.

Validation Architecture

Test Framework

Property Value
Framework xUnit 2.9.3 + Moq 4.20.72
Config file SharepointToolbox.Tests/SharepointToolbox.Tests.csproj
Quick run command dotnet test --filter "Category=Unit" --no-build
Full suite command dotnet test

Phase Requirements → Test Map

Req ID Behavior Test Type Automated Command File Exists?
BRAND-01 MSP logo saved to branding.json and reloaded correctly unit dotnet test --filter "FullyQualifiedName~BrandingServiceTests" --no-build Wave 0
BRAND-01 BrandingRepository round-trips BrandingSettings with MspLogo unit dotnet test --filter "FullyQualifiedName~BrandingRepositoryTests" --no-build Wave 0
BRAND-03 TenantProfile.ClientLogo serializes/deserializes in ProfileRepository unit dotnet test --filter "FullyQualifiedName~ProfileServiceTests" --no-build (extend existing)
BRAND-06 PNG file accepted, returns image/png MIME unit dotnet test --filter "FullyQualifiedName~BrandingServiceTests" --no-build Wave 0
BRAND-06 JPEG file accepted, returns image/jpeg MIME unit dotnet test --filter "FullyQualifiedName~BrandingServiceTests" --no-build Wave 0
BRAND-06 BMP file rejected with descriptive error unit dotnet test --filter "FullyQualifiedName~BrandingServiceTests" --no-build Wave 0
BRAND-06 File > 512 KB is auto-compressed (output ≤ 512 KB) unit dotnet test --filter "FullyQualifiedName~BrandingServiceTests" --no-build Wave 0
BRAND-06 File ≤ 512 KB is not modified unit dotnet test --filter "FullyQualifiedName~BrandingServiceTests" --no-build Wave 0
(UDIR-02 infra) GetUsersAsync follows all pages until exhausted unit dotnet test --filter "FullyQualifiedName~GraphUserDirectoryServiceTests" --no-build Wave 0
(UDIR-02 infra) GetUsersAsync respects CancellationToken mid-iteration unit dotnet test --filter "FullyQualifiedName~GraphUserDirectoryServiceTests" --no-build Wave 0

Sampling Rate

  • Per task commit: dotnet test --filter "Category=Unit" --no-build
  • Per wave merge: dotnet test
  • Phase gate: Full suite green before /gsd:verify-work

Wave 0 Gaps

  • SharepointToolbox.Tests/Services/BrandingServiceTests.cs — covers BRAND-06 + BRAND-01 import logic
  • SharepointToolbox.Tests/Services/BrandingRepositoryTests.cs — covers BRAND-01 persistence
  • SharepointToolbox.Tests/Services/GraphUserDirectoryServiceTests.cs — covers UDIR-02 infrastructure

(Extend existing ProfileServiceTests.cs to verify ClientLogo round-trip — covers BRAND-03)


Sources

Primary (HIGH confidence)

  • Codebase inspection — SettingsRepository.cs, ProfileRepository.cs, GraphUserSearchService.cs, GraphClientFactory.cs, App.xaml.cs, TenantProfile.cs, AppSettings.cs
  • SharepointToolbox.csproj — confirms Microsoft.Graph 5.74.0, no System.Drawing explicit reference needed (net10.0-windows)
  • SharepointToolbox.Tests.csproj — confirms xUnit 2.9.3, Moq 4.20.72 test stack
  • 10-CONTEXT.md — locked decisions, compression strategy, magic byte specs, model shapes

Secondary (MEDIUM confidence)

  • Microsoft.Graph 5.x SDK architecture — PageIterator<T, TPage> pattern confirmed in Graph SDK source and documentation; version 5.74.0 is current
  • System.Drawing.Common Windows availability — confirmed by .NET documentation: available on Windows, restricted on non-Windows since .NET 6

Tertiary (LOW confidence)

  • PageIterator.IterateAsync(CancellationToken) overload availability in 5.74.0 specifically — needs compile-time verification

Metadata

Confidence breakdown:

  • Standard stack: HIGH — confirmed from csproj; zero new packages
  • Architecture: HIGH — all patterns are direct clones of existing code in the repo
  • Magic byte detection: HIGH — PNG/JPEG signatures are stable specs
  • PageIterator pattern: MEDIUM — SDK version-specific overload needs verification at implementation time
  • Pitfalls: HIGH — identified from codebase inspection and known .NET behaviors

Research date: 2026-04-08 Valid until: 2026-05-08 (stable domain — Microsoft.Graph minor versions change rarely)