Skip to main content
Intune Commander uses a service-oriented architecture with dependency injection for core services and a consistent pattern for Graph API services. This document explains the service layer architecture.

Service Layers

Core Services (Singleton/Transient)

Registered in DI container via ServiceCollectionExtensions.AddIntuneCommanderCore():
public static IServiceCollection AddIntuneCommanderCore(this IServiceCollection services)
{
    // DataProtection for encryption
    services.AddDataProtection()
        .SetApplicationName("IntuneManager") // Compatibility constant
        .PersistKeysToFileSystem(new DirectoryInfo(keysPath));

    // Singleton services
    services.AddSingleton<IProfileEncryptionService, ProfileEncryptionService>();
    services.AddSingleton<IAuthenticationProvider, InteractiveBrowserAuthProvider>();
    services.AddSingleton<IntuneGraphClientFactory>();
    services.AddSingleton<ProfileService>(sp =>
        new ProfileService(encryption: sp.GetRequiredService<IProfileEncryptionService>()));
    services.AddSingleton<ICacheService>(sp =>
        new CacheService(sp.GetRequiredService<IDataProtectionProvider>()));

    // Transient services
    services.AddTransient<IExportService, ExportService>();

    return services;
}
Service Lifetimes:
ServiceLifetimePurpose
IAuthenticationProviderSingletonManages authentication logic
IntuneGraphClientFactorySingletonCreates GraphServiceClient instances
ProfileServiceSingletonManages tenant profiles
IProfileEncryptionServiceSingletonEncrypts/decrypts profile data
ICacheServiceSingletonLiteDB-based cache for Graph responses
IExportServiceTransientExport operations (new instance per operation)
MainWindowViewModelTransientMain view model

Graph API Services (Post-Authentication)

Important: Graph API services are NOT registered in the DI container. They are created 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
Creation Pattern:
private async Task ConnectToProfileAsync(TenantProfile profile)
{
    // Create authenticated GraphServiceClient
    var graphClient = await _graphClientFactory.CreateClientAsync(profile);
    
    // Instantiate Graph API services
    _configurationProfileService = new ConfigurationProfileService(graphClient);
    _compliancePolicyService = new CompliancePolicyService(graphClient);
    _settingsCatalogService = new SettingsCatalogService(graphClient);
    _endpointSecurityService = new EndpointSecurityService(graphClient);
    _administrativeTemplateService = new AdministrativeTemplateService(graphClient);
    // ... 25+ more services
    
    // Services are now tenant-specific and ready to use
}
All Graph service fields in MainWindowViewModel are nullable (IConfigurationProfileService?) and only populated after ConnectToProfileAsync succeeds.

Graph Service Pattern

Every Graph API service follows a consistent pattern:

Service Interface

public interface IConfigurationProfileService
{
    Task<List<DeviceConfiguration>> ListDeviceConfigurationsAsync(
        CancellationToken cancellationToken = default);
    
    Task<DeviceConfiguration?> GetDeviceConfigurationAsync(
        string id, 
        CancellationToken cancellationToken = default);
    
    Task<DeviceConfiguration> CreateDeviceConfigurationAsync(
        DeviceConfiguration config, 
        CancellationToken cancellationToken = default);
    
    Task<DeviceConfiguration> UpdateDeviceConfigurationAsync(
        DeviceConfiguration config, 
        CancellationToken cancellationToken = default);
    
    Task DeleteDeviceConfigurationAsync(
        string id, 
        CancellationToken cancellationToken = default);
    
    Task<List<DeviceConfigurationAssignment>> GetAssignmentsAsync(
        string configId, 
        CancellationToken cancellationToken = default);
}
Standard Methods:
  • List*Async() - Get all items (with manual pagination)
  • Get*Async(string id) - Get single item by ID
  • Create*Async(T item) - Create new item
  • Update*Async(T item) - Update existing item
  • Delete*Async(string id) - Delete item
  • GetAssignmentsAsync(string id) - Get assignments for item
All methods:
  • Are async and return Task or Task<T>
  • Accept CancellationToken cancellationToken = default as last parameter
  • Use nullable return types where appropriate (T? for Get methods)

Service Implementation

public class ConfigurationProfileService : IConfigurationProfileService
{
    private readonly GraphServiceClient _graphClient;

    public ConfigurationProfileService(GraphServiceClient graphClient)
    {
        _graphClient = graphClient;
    }

    public async Task<List<DeviceConfiguration>> ListDeviceConfigurationsAsync(
        CancellationToken cancellationToken = default)
    {
        var result = new List<DeviceConfiguration>();

        // CRITICAL: Manual pagination, never use PageIterator
        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;
    }

    public async Task<DeviceConfiguration?> GetDeviceConfigurationAsync(
        string id, 
        CancellationToken cancellationToken = default)
    {
        return await _graphClient.DeviceManagement.DeviceConfigurations[id]
            .GetAsync(cancellationToken: cancellationToken);
    }

    public async Task<DeviceConfiguration> CreateDeviceConfigurationAsync(
        DeviceConfiguration config, 
        CancellationToken cancellationToken = default)
    {
        var result = await _graphClient.DeviceManagement.DeviceConfigurations
            .PostAsync(config, cancellationToken: cancellationToken);

        return result ?? throw new InvalidOperationException(
            "Failed to create device configuration");
    }

    public async Task<DeviceConfiguration> UpdateDeviceConfigurationAsync(
        DeviceConfiguration config, 
        CancellationToken cancellationToken = default)
    {
        var id = config.Id ?? throw new ArgumentException(
            "Device configuration must have an ID for update");

        var result = await _graphClient.DeviceManagement.DeviceConfigurations[id]
            .PatchAsync(config, cancellationToken: cancellationToken);

        // Graph PATCH sometimes returns null on success
        return await GraphPatchHelper.PatchWithGetFallbackAsync(
            result, 
            () => GetDeviceConfigurationAsync(id, cancellationToken), 
            "device configuration");
    }

    public async Task DeleteDeviceConfigurationAsync(
        string id, 
        CancellationToken cancellationToken = default)
    {
        await _graphClient.DeviceManagement.DeviceConfigurations[id]
            .DeleteAsync(cancellationToken: cancellationToken);
    }

    public async Task<List<DeviceConfigurationAssignment>> GetAssignmentsAsync(
        string configId, 
        CancellationToken cancellationToken = default)
    {
        var response = await _graphClient.DeviceManagement
            .DeviceConfigurations[configId]
            .Assignments
            .GetAsync(cancellationToken: cancellationToken);

        return response?.Value ?? [];
    }
}

Manual Pagination Pattern

CRITICAL: Always manually implement pagination. Never use PageIterator as it silently truncates results on some tenants.
public async Task<List<T>> ListItemsAsync(CancellationToken cancellationToken = default)
{
    var result = new List<T>();

    // Set appropriate page size for endpoint
    var response = await _graphClient.SomeEndpoint
        .GetAsync(req => req.QueryParameters.Top = 200, cancellationToken);

    // Loop through all pages
    while (response != null)
    {
        if (response.Value != null)
            result.AddRange(response.Value);

        // Check for next page
        if (!string.IsNullOrEmpty(response.OdataNextLink))
        {
            response = await _graphClient.SomeEndpoint
                .WithUrl(response.OdataNextLink)
                .GetAsync(cancellationToken: cancellationToken);
        }
        else
        {
            break;
        }
    }

    return result;
}
Endpoint-specific page sizes:
  • configurationPolicies (Settings Catalog): $top=100 (Cosmos DB cursor stability)
  • windowsQualityUpdateProfiles, windowsDriverUpdateProfiles: $top=200 (hard API cap)
  • All other list endpoints: $top=999 (or default 200)
See src/Intune.Commander.Core/Services/SettingsCatalogService.cs for an example with smaller page size.

Key Services

ProfileService

Purpose: Manages tenant profile storage and retrieval. Location: src/Intune.Commander.Core/Services/ProfileService.cs Storage:
  • File: %LOCALAPPDATA%\Intune.Commander\profiles.json
  • Encrypted with INTUNEMANAGER_ENC: prefix
  • Contains list of TenantProfile objects
  • Auto-migrates from legacy IntuneManager path on first run
Key Methods:
Task<ProfileStore> LoadAsync()
Task SaveAsync(ProfileStore store)
Task MigrateLegacyProfileAsync()
Profile Schema:
{
  "profiles": [
    {
      "id": "guid",
      "name": "Contoso-Prod-GCCHigh",
      "tenantId": "tenant-guid",
      "clientId": "app-guid",
      "cloud": "GCCHigh",
      "authMethod": "Interactive",
      "clientSecret": null,
      "lastUsed": "2025-02-14T10:30:00Z"
    }
  ],
  "activeProfileId": "guid"
}

ProfileEncryptionService

Purpose: Encrypts and decrypts sensitive profile data using DataProtection. Location: src/Intune.Commander.Core/Services/ProfileEncryptionService.cs Key Methods:
string Encrypt(string plaintext)
string Decrypt(string ciphertext)
bool IsEncrypted(string data)
Encryption:
  • Uses Microsoft.AspNetCore.DataProtection
  • Keys stored at %LOCALAPPDATA%\Intune.Commander\keys
  • DPAPI-protected on Windows
  • File-system protected on macOS/Linux

CacheService

Purpose: LiteDB-based cache for Graph API responses. Location: src/Intune.Commander.Core/Services/CacheService.cs Storage:
  • File: %LOCALAPPDATA%\Intune.Commander\cache.db (AES-encrypted)
  • Password: Generated once, stored encrypted in cache-key.bin
Key Methods:
Task<T?> GetAsync<T>(string tenantId, string key)
Task SetAsync<T>(string tenantId, string key, T value, TimeSpan? ttl = null)
Task RemoveAsync(string tenantId, string key)
Task ClearAsync(string tenantId)
Default TTL: 24 hours Usage:
var cacheKey = $"{profile.TenantId}:DeviceConfigurations";
var cached = await _cacheService.GetAsync<List<DeviceConfiguration>>(profile.TenantId, cacheKey);

if (cached == null)
{
    var data = await _configurationProfileService.ListDeviceConfigurationsAsync();
    await _cacheService.SetAsync(profile.TenantId, cacheKey, data);
    return data;
}

return cached;

IntuneGraphClientFactory

Purpose: Creates authenticated GraphServiceClient instances with correct cloud endpoints. Location: src/Intune.Commander.Core/Auth/IntuneGraphClientFactory.cs Key Method:
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}")
    };
    
    var httpClient = new HttpClient();
    httpClient.BaseAddress = new Uri(endpoints.GraphBaseUrl);
    
    return new GraphServiceClient(httpClient, credential, scopes);
}
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

ExportService

Purpose: Exports Intune policies to JSON files. Location: src/Intune.Commander.Core/Services/ExportService.cs Export Format:
ExportFolder/
├── DeviceConfigurations/
│   ├── Policy1.json
│   └── Policy2.json
├── CompliancePolicies/
│   └── Policy3.json
├── SettingsCatalog/
│   └── Policy4.json
└── migration-table.json
Key Methods:
Task ExportAsync(string outputPath, ExportOptions options, IProgress<string>? progress = null)
Progress Reporting:
var progress = new Progress<string>(msg => Console.WriteLine(msg));
await exportService.ExportAsync(path, options, progress);

ImportService

Purpose: Imports policies from JSON files into target tenant. Location: src/Intune.Commander.Core/Services/ImportService.cs Key Methods:
Task ImportAsync(string sourcePath, ImportOptions options, IProgress<string>? progress = null)
Features:
  • Reads PowerShell export format
  • Creates migration table mapping old IDs → new IDs
  • Updates assignment references
  • Preserves object relationships

All Graph API Services

Intune Commander includes 30+ Graph API services:

Device Configuration

  • ConfigurationProfileService - Device configurations
  • SettingsCatalogService - Settings catalog policies
  • AdministrativeTemplateService - Admin templates (ADMX)
  • AdmxFileService - Custom ADMX file uploads

Compliance

  • CompliancePolicyService - Compliance policies
  • ComplianceScriptService - Custom compliance scripts

Endpoint Security

  • EndpointSecurityService - Endpoint security policies (Antivirus, Firewall, etc.)

Applications

  • ApplicationService - Mobile apps
  • AppProtectionPolicyService - MAM policies
  • ManagedAppConfigurationService - App configuration policies

Scripts

  • DeviceManagementScriptService - PowerShell scripts (Windows)
  • DeviceHealthScriptService - Proactive remediation scripts
  • DeviceShellScriptService - Shell scripts (macOS)
  • MacCustomAttributeService - macOS custom attributes

Updates

  • QualityUpdateProfileService - Windows quality updates
  • FeatureUpdateProfileService - Windows feature updates
  • DriverUpdateProfileService - Windows driver updates

Enrollment

  • EnrollmentConfigurationService - Enrollment configurations
  • AutopilotService - Windows Autopilot profiles
  • AppleDepService - Apple DEP tokens

Identity & Access

  • ConditionalAccessPolicyService - Conditional Access policies
  • NamedLocationService - Named locations
  • AuthenticationStrengthService - Authentication strengths
  • AuthenticationContextService - Authentication contexts

Tenant Administration

  • RoleDefinitionService - RBAC role definitions
  • ScopeTagService - Scope tags
  • AssignmentFilterService - Assignment filters
  • DeviceCategoryService - Device categories
  • NotificationTemplateService - Notification templates
  • TermsAndConditionsService - Terms and conditions
  • IntuneBrandingService - Company portal branding

Directory

  • GroupService - Azure AD groups
  • UserService - Azure AD users

Cloud PC

  • CloudPcProvisioningService - Cloud PC provisioning policies
  • CloudPcUserSettingsService - Cloud PC user settings

Supporting Services

  • AssignmentCheckerService - Validates assignments
  • DirectoryObjectResolver - Resolves group/user names
  • PermissionCheckService - Checks JWT token permissions
  • PolicySetService - Policy sets
All services follow the same constructor pattern:
public SomeService(GraphServiceClient graphClient)
{
    _graphClient = graphClient;
}

Testing Services

Unit Testing Graph Services

Problem: GraphServiceClient is sealed and cannot be mocked. Solution: Use reflection-based contract tests to verify interface conformance:
[Fact]
public void Service_ImplementsInterface()
{
    var serviceType = typeof(ConfigurationProfileService);
    var interfaceType = typeof(IConfigurationProfileService);
    
    Assert.True(interfaceType.IsAssignableFrom(serviceType));
}

[Fact]
public void AllMethods_AcceptCancellationToken()
{
    var methods = typeof(IConfigurationProfileService).GetMethods();
    
    foreach (var method in methods)
    {
        var parameters = method.GetParameters();
        var lastParam = parameters.LastOrDefault();
        
        Assert.NotNull(lastParam);
        Assert.Equal(typeof(CancellationToken), lastParam.ParameterType);
        Assert.True(lastParam.IsOptional);
    }
}

Integration Testing

For services that require actual Graph API calls:
[Trait("Category", "Integration")]
public class ConfigurationProfileServiceIntegrationTests : GraphIntegrationTestBase
{
    [Fact]
    public async Task ListDeviceConfigurationsAsync_ReturnsData()
    {
        if (ShouldSkip()) return;
        
        var service = new ConfigurationProfileService(GraphClient);
        var result = await service.ListDeviceConfigurationsAsync();
        
        Assert.NotNull(result);
    }
}
See Testing documentation for complete testing patterns.

Service Conventions

Naming

  • Interface: I{Object}Service (e.g., IConfigurationProfileService)
  • Implementation: {Object}Service (e.g., ConfigurationProfileService)
  • File location: src/Intune.Commander.Core/Services/{Object}Service.cs

Constructor

public SomeService(GraphServiceClient graphClient)
{
    _graphClient = graphClient;
}

Method Naming

  • List: List{Objects}Async()Task<List<T>>
  • Get: Get{Object}Async(string id)Task<T?>
  • Create: Create{Object}Async(T item)Task<T>
  • Update: Update{Object}Async(T item)Task<T>
  • Delete: Delete{Object}Async(string id)Task
  • Assignments: GetAssignmentsAsync(string id)Task<List<Assignment>>

Cancellation

All async methods accept CancellationToken cancellationToken = default as the last parameter.

Nullability

  • Get methods return Task<T?> (nullable)
  • List methods return Task<List<T>> (empty list, not null)
  • Create/Update return Task<T> (non-nullable, throw on failure)

Build docs developers (and LLMs) love