Skip to main content
The Cache Service provides encrypted, TTL-based persistence of Graph API responses using LiteDB. Caching reduces API calls and improves performance when navigating between views.

ICacheService

Thread-safe cache interface for storing and retrieving Graph data per tenant.
public interface ICacheService : IDisposable
{
    List<T>? Get<T>(string tenantId, string dataType);
    
    void Set<T>(string tenantId, string dataType, List<T> items, TimeSpan? ttl = null);
    
    void Invalidate(string tenantId, string? dataType = null);
    
    int CleanupExpired();
    
    (DateTime CachedAt, int ItemCount)? GetMetadata(string tenantId, string dataType);
}
See ICacheService.cs:7 for the complete interface.

Storage Architecture

Database Location

%LocalAppData%/Intune.Commander/
  cache.db          # Encrypted LiteDB database
  cache-key.bin     # AES key (encrypted via DataProtection)

Encryption

  1. Database password: Random 32-byte value, base64-encoded
  2. Password storage: Encrypted using Microsoft.AspNetCore.DataProtection with purpose Intune.Commander.Cache.Password.v1
  3. DataProtection keys: Stored at %LocalAppData%/Intune.Commander/keys
If the password file is corrupted or DataProtection keys are lost, the cache is wiped and regenerated automatically. See CacheService.cs:261 for password management logic.

Cache Schema

public class CacheEntry
{
    public string Id { get; set; }              // "{TenantId}|{DataType}"
    public string TenantId { get; set; }        // Azure AD tenant ID
    public string DataType { get; set; }        // e.g., "DeviceConfigurations"
    public string JsonData { get; set; }        // Serialized List<T>
    public DateTime CachedAtUtc { get; set; }   // When cached
    public DateTime ExpiresAtUtc { get; set; }  // When expires
    public int ItemCount { get; set; }          // Number of items (for logging)
}
Indexes:
  • TenantId - for tenant-wide invalidation
  • ExpiresAtUtc - for cleanup queries
See CacheEntry.cs:7 for the model definition.

Methods

Get

Retrieves cached data for a tenant and data type.
List<T>? Get<T>(string tenantId, string dataType)
tenantId
string
required
Azure AD tenant ID
dataType
string
required
Cache key (e.g., "DeviceConfigurations", "CompliancePolicies")
return
List<T>?
Cached data, or null if missing/expired/corrupted
Behavior:
  • Returns null if entry doesn’t exist
  • Returns null if entry is expired (and deletes the stale entry)
  • Returns null if deserialization fails (schema change, corruption)
  • Deserializes with polymorphic type resolution for Graph models
Example:
var cached = cacheService.Get<DeviceConfiguration>(tenantId, "DeviceConfigurations");

if (cached != null)
{
    Console.WriteLine($"Using cached data: {cached.Count} items");
    return cached;
}

var fresh = await configService.ListDeviceConfigurationsAsync(ct);
cacheService.Set(tenantId, "DeviceConfigurations", fresh);
return fresh;
See CacheService.cs:71 for implementation.

Set

Stores data in the cache with optional TTL.
void Set<T>(string tenantId, string dataType, List<T> items, TimeSpan? ttl = null)
tenantId
string
required
Azure AD tenant ID
dataType
string
required
Cache key
items
List<T>
required
Data to cache
ttl
TimeSpan?
Time to live. Defaults to 24 hours if not specified.
Behavior:
  • Upserts the cache entry (replaces if exists)
  • Serializes with runtime type information to preserve polymorphic properties
  • Sets ExpiresAtUtc = DateTime.UtcNow + ttl
Example with custom TTL:
// Cache group data for only 1 hour (groups change frequently)
var groups = await groupService.ListAssignedGroupsAsync(ct);
cacheService.Set(tenantId, "AssignedGroups", groups, TimeSpan.FromHours(1));
See CacheService.cs:98 for implementation.

Invalidate

Removes cached data for a tenant.
void Invalidate(string tenantId, string? dataType = null)
tenantId
string
required
Azure AD tenant ID
dataType
string?
Specific data type to invalidate, or null to clear all data for the tenant
Example:
// Invalidate compliance policies after creating a new one
await complianceService.CreateCompliancePolicyAsync(policy, ct);
cacheService.Invalidate(tenantId, "CompliancePolicies");

// Invalidate all cached data for the tenant (e.g., on sign-out)
cacheService.Invalidate(tenantId);
See CacheService.cs:117 for implementation.

CleanupExpired

Removes all expired entries from the database.
int CleanupExpired()
return
int
Number of entries deleted
Example:
// Run cleanup on app startup or periodically
var deletedCount = cacheService.CleanupExpired();
Console.WriteLine($"Cleaned up {deletedCount} expired cache entries");
See CacheService.cs:129 for implementation.

GetMetadata

Retrieves cache metadata without deserializing the data.
(DateTime CachedAt, int ItemCount)? GetMetadata(string tenantId, string dataType)
tenantId
string
required
Azure AD tenant ID
dataType
string
required
Cache key
CachedAt
DateTime
When the data was cached (UTC)
ItemCount
int
Number of items in the cached collection
return
(DateTime, int)?
Tuple with metadata, or null if entry is missing/expired
Example:
var metadata = cacheService.GetMetadata(tenantId, "DeviceConfigurations");

if (metadata.HasValue)
{
    var age = DateTime.UtcNow - metadata.Value.CachedAt;
    Console.WriteLine($"Cache age: {age.TotalMinutes:F1} minutes");
    Console.WriteLine($"Item count: {metadata.Value.ItemCount}");
}
else
{
    Console.WriteLine("No cached data available");
}
See CacheService.cs:142 for implementation.

Polymorphic Deserialization

The cache service uses OData type discriminators to preserve derived types during serialization/deserialization.

How It Works

  1. Serialization: Each item is serialized with its runtime type (not generic T)
  2. Deserialization: Reads the @odata.type field from JSON and resolves to the correct C# type
  3. Type cache: Resolved types are cached in a ConcurrentDictionary for performance
Example JSON:
[
  {
    "@odata.type": "#microsoft.graph.windows10GeneralConfiguration",
    "id": "12345",
    "displayName": "Windows 10 Policy",
    "passwordRequired": true
  },
  {
    "@odata.type": "#microsoft.graph.iosGeneralDeviceConfiguration",
    "id": "67890",
    "displayName": "iOS Policy",
    "passcodeRequired": true
  }
]
When deserializing List<DeviceConfiguration>, the cache:
  1. Sees @odata.type: #microsoft.graph.windows10GeneralConfiguration
  2. Resolves to Windows10GeneralConfiguration type
  3. Deserializes with correct derived type
  4. Returns DeviceConfiguration list with runtime types preserved
See CacheService.cs:203 for polymorphic deserialization logic.

Usage Patterns

Read-Through Cache

public async Task<List<DeviceConfiguration>> GetConfigurationsAsync(
    string tenantId,
    IConfigurationProfileService configService,
    CancellationToken ct)
{
    const string CacheKey = "DeviceConfigurations";
    
    // Try cache first
    var cached = _cacheService.Get<DeviceConfiguration>(tenantId, CacheKey);
    if (cached != null)
        return cached;
    
    // Cache miss - fetch from Graph API
    var fresh = await configService.ListDeviceConfigurationsAsync(ct);
    
    // Store in cache with 24-hour TTL (default)
    _cacheService.Set(tenantId, CacheKey, fresh);
    
    return fresh;
}

Cache Invalidation on Write

public async Task CreateCompliancePolicyAsync(
    string tenantId,
    DeviceCompliancePolicy policy,
    ICompliancePolicyService complianceService,
    CancellationToken ct)
{
    // Create the policy
    var created = await complianceService.CreateCompliancePolicyAsync(policy, ct);
    
    // Invalidate cache so next read fetches updated list
    _cacheService.Invalidate(tenantId, "CompliancePolicies");
    
    return created;
}

Tenant Isolation

The cache automatically isolates data by tenant ID:
// Switch between tenant profiles without cache pollution
var tenant1Configs = _cacheService.Get<DeviceConfiguration>("tenant-1-guid", "DeviceConfigurations");
var tenant2Configs = _cacheService.Get<DeviceConfiguration>("tenant-2-guid", "DeviceConfigurations");

Performance Considerations

When to Cache

Good candidates:
  • Device configurations (low change rate)
  • Compliance policies (low change rate)
  • Applications (low change rate)
  • Scope tags, role definitions (rarely change)
Poor candidates:
  • Managed devices (high change rate, real-time data desired)
  • App install status (real-time)
  • Compliance reports (real-time)

TTL Recommendations

Data TypeRecommended TTLReason
Device configurations24 hours (default)Low change rate
Groups1 hourModerate change rate
Managed devicesDo not cacheReal-time data
Scope tags7 daysRarely change

Cache Size

The LiteDB database is unbounded but self-manages via TTL expiration. Monitor cache size:
var dbPath = Path.Combine(
    Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
    "Intune.Commander",
    "cache.db");

var fileSize = new FileInfo(dbPath).Length;
Console.WriteLine($"Cache size: {fileSize / 1024 / 1024:F2} MB");

Troubleshooting

Cache Corruption

If the cache database or key file is corrupted:
  1. The service detects unreadable data during Get()
  2. Returns null and deletes the stale entry
  3. Next Set() overwrites with fresh data
If the entire database is corrupt (LiteDB error on open):
  1. Delete cache.db and cache-key.bin manually
  2. Restart the application
  3. New cache will be created automatically

Key Rotation

If DataProtection keys are rotated (e.g., machine reinstall):
  1. The password file can’t be decrypted
  2. CacheService constructor catches the exception
  3. Deletes cache.db, cache.db-log, and cache-key.bin
  4. Generates a new password and database
See CacheService.cs:261 for key rotation recovery logic.

Migration from Legacy IntuneManager

The cache uses a new database (no migration needed). Legacy cache at %LocalAppData%/IntuneManager/cache.db is not migrated and can be deleted manually.

Next Steps

Export Service

Export configurations to JSON for backup and migration

Graph Services

Return to Graph API service documentation

Build docs developers (and LLMs) love