Skip to main content
Intune Commander follows a layered architecture with clear separation between business logic and UI concerns. This document outlines the key architectural decisions and patterns that guide the codebase.

High-Level Architecture

Project Structure

src/
  Intune.Commander.Core/    # Business logic (.NET 10 class library)
    Auth/                    # Authentication providers and factory
    Models/                  # Domain models, enums, DTOs
    Services/                # Graph API services, caching, import/export
    Extensions/              # ServiceCollectionExtensions (DI setup)
  
  Intune.Commander.Desktop/  # Avalonia UI
    Views/                   # AXAML UI definitions
    ViewModels/              # MVVM view models
    Services/                # UI-specific services (DebugLogService)
    Converters/              # Value converters for data binding

tests/
  Intune.Commander.Core.Tests/  # xUnit tests mirroring src structure

Separation of Concerns

Core Library (Intune.Commander.Core):
  • Platform-agnostic business logic
  • Microsoft Graph API integration
  • Authentication and multi-cloud support
  • Data models and services
  • Export/import functionality
  • No UI dependencies
Desktop Application (Intune.Commander.Desktop):
  • Avalonia-based UI
  • MVVM view models
  • UI-specific services (logging, dialogs)
  • Data binding and converters
  • References Core library

Key Architecture Decisions

1. Azure.Identity over MSAL

Decision: Use Azure.Identity library instead of raw MSAL. Rationale:
  • Modern Microsoft-recommended approach
  • Built-in multi-cloud support
  • TokenCredential abstraction for multiple credential types
  • Cleaner API surface
  • Automatic credential fallback chains
Implementation:
// From: src/Intune.Commander.Core/Auth/IntuneGraphClientFactory.cs
public async Task<GraphServiceClient> CreateClientAsync(TenantProfile profile)
{
    var endpoints = CloudEndpoints.GetEndpoints(profile.Cloud);
    
    TokenCredential credential = profile.AuthMethod switch
    {
        AuthMethod.Interactive => new InteractiveBrowserCredential(
            new InteractiveBrowserCredentialOptions
            {
                TenantId = profile.TenantId,
                ClientId = profile.ClientId,
                AuthorityHost = endpoints.AuthorityHost
            }),
        AuthMethod.ClientSecret => new ClientSecretCredential(
            profile.TenantId,
            profile.ClientId,
            profile.ClientSecret,
            new TokenCredentialOptions { AuthorityHost = endpoints.AuthorityHost }
        ),
        _ => throw new NotSupportedException($"Auth method {profile.AuthMethod} not supported")
    };
    
    return new GraphServiceClient(credential, scopes, endpoints.GraphBaseUrl);
}

2. Direct Use of Microsoft.Graph.Beta SDK Models

Decision: Use Microsoft.Graph.Beta SDK models directly instead of creating custom DTOs. Rationale:
  • Strongly typed models
  • Automatic updates when Graph API changes
  • No manual mapping code required
  • Built-in serialization
  • Reduced maintenance burden
Exception Cases:
  • Export/Import DTOs when SDK models have non-serializable properties
  • Custom wrapper models for specific UI binding requirements
See Technology Stack for the complete dependency list.

3. Multi-Cloud Architecture

Decision: Support multiple Azure cloud environments with separate app registrations per cloud. Cloud Endpoints:
CloudGraph EndpointAuthority Host
Commercialhttps://graph.microsoft.comAzureAuthorityHosts.AzurePublicCloud
GCChttps://graph.microsoft.comAzureAuthorityHosts.AzurePublicCloud
GCC-Highhttps://graph.microsoft.usAzureAuthorityHosts.AzureGovernment
DoDhttps://dod-graph.microsoft.usAzureAuthorityHosts.AzureGovernment
Rationale:
  • GCC-High and DoD require registrations in separate Azure portals
  • Environment isolation and security
  • Simpler permission management
  • Avoids cross-cloud authentication errors
Implementation: Each TenantProfile stores cloud-specific ClientId and the cloud type determines the correct endpoints. See src/Intune.Commander.Core/Models/CloudEndpoints.cs for the implementation.

4. Graph Services Created Post-Authentication

Decision: Graph API services are NOT registered in dependency injection. They are instantiated in MainWindowViewModel after successful authentication. Rationale:
  • Services require an authenticated GraphServiceClient
  • User may switch between multiple tenant profiles
  • Services are tenant-specific, not application-wide
  • Simpler lifecycle management
Implementation:
// From: src/Intune.Commander.Desktop/ViewModels/MainWindowViewModel.cs
private async Task ConnectToProfileAsync(TenantProfile profile)
{
    var graphClient = await _graphClientFactory.CreateClientAsync(profile);
    
    // Services created here, after authentication
    _configurationProfileService = new ConfigurationProfileService(graphClient);
    _compliancePolicyService = new CompliancePolicyService(graphClient);
    _settingsCatalogService = new SettingsCatalogService(graphClient);
    // ... 30+ more services
}
All Graph service interfaces (IConfigurationProfileService, ICompliancePolicyService, etc.) follow the same constructor pattern: new XxxService(GraphServiceClient graphClient).

5. Profile and Cache Encryption

Decision: Use Microsoft.AspNetCore.DataProtection for encrypting sensitive data at rest. Rationale:
  • Cross-platform encryption using a single consistent API
  • DPAPI-protected keys on Windows
  • File-system protected keys on macOS/Linux
  • Keys persist in user’s local app data directory
  • No need to manage encryption keys manually
Profile Storage:
  • Location: %LOCALAPPDATA%\Intune.Commander\profiles.json
  • Encrypted with INTUNEMANAGER_ENC: prefix
  • ClientSecret stored encrypted in the profile
  • Plaintext files automatically migrated on next save
Cache Storage:
  • Location: %LOCALAPPDATA%\Intune.Commander\cache.db (LiteDB)
  • AES-encrypted database
  • Password generated once and stored encrypted in cache-key.bin
  • 24-hour default TTL per cache entry
  • Keyed by tenant ID + data type
See src/Intune.Commander.Core/Services/ProfileEncryptionService.cs and CacheService.cs.

6. Manual Pagination Pattern

Decision: Always manually implement pagination for Graph API list requests. Never use PageIterator. Rationale:
  • PageIterator silently truncates results on some tenants
  • Large tenants require proper page size tuning
  • Explicit control over @odata.nextLink handling
  • Better error handling and retry logic
Standard Pattern:
// From: src/Intune.Commander.Core/Services/ConfigurationProfileService.cs
public async Task<List<DeviceConfiguration>> ListDeviceConfigurationsAsync(
    CancellationToken cancellationToken = default)
{
    var result = new List<DeviceConfiguration>();

    var response = await _graphClient.DeviceManagement.DeviceConfigurations
        .GetAsync(req => req.QueryParameters.Top = 200, cancellationToken);

    while (response != null)
    {
        if (response.Value != null)
            result.AddRange(response.Value);

        if (!string.IsNullOrEmpty(response.OdataNextLink))
        {
            response = await _graphClient.DeviceManagement.DeviceConfigurations
                .WithUrl(response.OdataNextLink)
                .GetAsync(cancellationToken: cancellationToken);
        }
        else
        {
            break;
        }
    }

    return result;
}
Endpoint-specific page size limits:
  • configurationPolicies (Settings Catalog): $top=100 (Cosmos DB cursor stability)
  • windowsQualityUpdateProfiles, windowsDriverUpdateProfiles: $top=200 (hard API cap)
  • All other list endpoints: $top=999
See the Services documentation for more details on the service pattern.

7. MVVM with CommunityToolkit.Mvvm

Decision: Use MVVM pattern with CommunityToolkit.Mvvm source generators. Rationale:
  • Industry standard for Avalonia/WPF applications
  • Source generators eliminate boilerplate code
  • Clean separation of UI and business logic
  • Testable view models
  • Compile-time safety with x:DataType bindings
Implementation:
public partial class LoginViewModel : ObservableObject
{
    [ObservableProperty]
    private string _tenantId = string.Empty;
    
    [ObservableProperty]
    private CloudEnvironment _selectedCloud = CloudEnvironment.Commercial;
    
    [RelayCommand]
    private async Task ConnectAsync()
    {
        // Command implementation
    }
}
The [ObservableProperty] attribute generates the full property with INotifyPropertyChanged support. The [RelayCommand] attribute generates an ICommand implementation. See src/Intune.Commander.Desktop/ViewModels/ for examples.

Error Handling Strategy

Graph API Errors

Common Graph API errors are translated to user-friendly messages:
Graph ErrorUser Message
Forbidden (403)“Missing permission: DeviceManagementConfiguration.ReadWrite.All”
TooManyRequests (429)“Request throttled. Retrying in X seconds…”
Unauthorized (401)“Session expired. Please sign in again.”
NotFound (404)“Object not found. It may have been deleted.”
InternalServerError (500)Cosmos DB skip-token cursor bug on large page requests; mitigated with smaller $top and exponential-backoff retry

Retry Strategy

  • Exponential backoff: 1s, 2s, 4s, 8s, 16s
  • Respect Retry-After headers
  • Maximum 5 retries
  • User cancellation support via CancellationToken

Export/Import Format

Directory Structure

Decision: Maintain PowerShell JSON compatibility (read-only). Format:
ExportFolder/
├── DeviceConfigurations/
│   ├── Policy1.json
│   └── Policy2.json
├── CompliancePolicies/
│   └── Policy3.json
├── SettingsCatalog/
│   └── Policy4.json
└── migration-table.json
Migration Table:
{
  "objectType": "DeviceConfiguration",
  "originalId": "source-tenant-id",
  "newId": "destination-tenant-id",
  "name": "Policy Name",
  "exportedAt": "2025-02-14T10:00:00Z"
}
Rationale:
  • Users may have existing PowerShell exports
  • Migration path from PowerShell version
  • Proven format structure
  • .NET version can read PowerShell exports (backward compatibility)
  • PowerShell version doesn’t need to read .NET exports (forward compatibility not required)

Performance Considerations

Graph API Optimization

  1. Batch requests where supported (future enhancement)
  2. Select queries - only request needed properties
  3. Filter queries - reduce payload size
  4. Parallel requests with concurrency limits
  5. LiteDB cache - 24-hour TTL reduces redundant API calls

UI Responsiveness

  1. Async/await for all I/O operations
  2. Background tasks for bulk operations
  3. Progress reporting via IProgress<T>
  4. Cancellation tokens for long operations
  5. Lazy loading - data loaded only when user navigates to a category
  6. Fire-and-forget for non-blocking UI loads: _ = LoadDataAsync();
Critical Rule: Never block the UI thread with .Result, .Wait(), or .GetAwaiter().GetResult()

Memory Management

  1. Streaming for large exports (future enhancement)
  2. Dispose Graph clients properly
  3. ObservableCollection size limits (e.g., DebugLogService capped at 2000 entries)

Security Considerations

Token Storage

  • Never log access tokens
  • Clear tokens on logout
  • Encrypted profile storage
  • No secrets in config files or source code

Certificate Handling

  • Certificate-based auth is deferred (not yet implemented)
  • When implemented: store thumbprints only, not private keys
  • Use Windows certificate store for private key storage

Network Security

  • HTTPS only
  • Certificate validation enabled
  • No proxy credential storage

Technology Constraints

.NET Version

Decision: .NET 10 (LTS through November 2026) Rationale:
  • Long-term support
  • Latest performance improvements
  • Required for latest Avalonia versions
  • C# 12 language features

C# Language Features Used

  • Primary constructors: public class MyService(GraphServiceClient client)
  • Collection expressions: return response?.Value ?? [];
  • Required members: required string TenantId { get; init; }
  • File-scoped namespaces: namespace Intune.Commander.Core.Services;
  • Nullable reference types: Enabled everywhere

Minimum Requirements

OS: Windows 10 1809+ (initial target), Linux and macOS support via Avalonia
RAM: 512MB minimum, 1GB recommended
.NET Runtime: Bundled with app (self-contained deployment)

Logging Strategy

DebugLogService

Implementation:
  • DebugLogService.Instance singleton
  • ObservableCollection<string> Entries (capped at 2000)
  • All log dispatches to UI thread via Dispatcher.UIThread.Post
  • Exposed in UI via DebugLogWindow
Usage:
DebugLog.Log("Auth", "Connecting to tenant...");
DebugLog.LogError("Export", $"Failed to export policy: {ex.Message}");
Note: No file-based logging is currently implemented. The plan to adopt Serilog is deferred. See src/Intune.Commander.Desktop/Services/DebugLogService.cs.
  • Technology Stack - Complete list of dependencies and versions
  • Services - Service architecture and DI patterns
  • Testing - Unit and integration test strategies
  • Building - Build commands and development workflow

Build docs developers (and LLMs) love