How Caching Works
Initial Connection
When you first connect to a tenant:- The app checks if cached data exists for this tenant
- If cached data is found and not expired, it’s loaded instantly from disk
- If no cache exists or cache is expired, the app fetches from Graph API
- Fresh data is cached for future use
Cache is per-tenant. Each tenant you connect to has its own isolated cache.
Lazy Loading and Cache
For lazy-loaded object types (Conditional Access, Endpoint Security, etc.):- You navigate to the category for the first time
- The app checks the cache
- If cached and not expired, data loads instantly
- If not cached, the app fetches from Graph API and caches the result
Cache Keys
Each object type has a unique cache key:DeviceConfigurationsCompliancePoliciesApplicationsSettingsCatalogConditionalAccessEndpointSecurityAppAssignmentsDynamicGroupsAssignedGroups- etc.
{TenantId}|{DataType}
Example: 12345678-1234-1234-1234-123456789012|DeviceConfigurations
Cache Storage
Database Location
Cache is stored in:Encryption
The cache database uses AES encryption via LiteDB’s built-in encryption:- On first run, a random 32-byte password is generated
- The password is encrypted via
Microsoft.AspNetCore.DataProtectionand stored in: - DataProtection uses Windows DPAPI (user-scoped) to protect the password
- The cache database is encrypted with this password
Cache encryption is transparent—you never need to enter a password. The app handles it automatically.
Cache Key Rotation
If DataProtection keys are rotated (e.g., Windows user profile rebuilt), the cache password becomes unreadable:- The app detects decryption failure
- Both
cache.dbandcache-key.binare deleted - A new cache and password are created
- Data is re-fetched from Graph API on next connection
Cache Time-to-Live (TTL)
Default TTL
All cached data has a 24-hour TTL.- Data cached at 10:00 AM on Monday expires at 10:00 AM on Tuesday
- Expired data is automatically deleted on next access
- You don’t need to manually clear expired cache
Per-Object TTL
All object types use the same 24-hour TTL. There is no custom TTL per type.Cache Expiration Behavior
When you connect to a tenant:- The app checks the cache timestamp for each data type
- If
CachedAtUtc + 24 hours < UtcNow, the entry is expired - Expired entries are deleted and fresh data is fetched from Graph API
Cache Age Indicator
The status bar shows cache age when data is loaded from cache:Cache Operations
Manual Refresh
To bypass cache and force a fresh fetch from Graph API:
Refresh reloads all currently-loaded object types, not just the selected category.
Cache Invalidation
After a Refresh:- Fresh data is fetched from Graph API
- The cache is updated with the new data
- The cache timestamp is reset to
UtcNow - TTL starts over (expires in 24 hours)
Cache Cleanup
The app automatically deletes expired cache entries when:- You connect to a tenant (cleans that tenant’s expired entries)
- You access a specific data type (cleans that data type if expired)
Cache Behavior by Scenario
Scenario: First Launch
- No cache exists
- App fetches all core types from Graph API
- Data is cached with 24-hour TTL
- Subsequent connections load instantly from cache
Scenario: Reconnect Within 24 Hours
- Cache exists and is not expired
- App loads data instantly from cache
- Status bar shows: “All data loaded from cache — skipping Graph refresh”
- No Graph API calls are made
Scenario: Reconnect After 24 Hours
- Cache exists but is expired
- App deletes expired entries
- Fresh data is fetched from Graph API
- Cache is updated with new data
Scenario: Partial Cache Hit
- Some data types are cached and fresh (e.g., Device Configs)
- Other data types are expired or missing (e.g., Compliance Policies)
- App loads cached data for fresh types
- App fetches expired/missing types from Graph API
- Status bar shows: “Partial cache hit (4/28) — refreshing from Graph”
Scenario: Navigate to Lazy-Loaded Category
- You navigate to Conditional Access (lazy-loaded)
- App checks if
ConditionalAccesscache key exists and is fresh - If yes, data loads instantly from cache
- If no, app fetches from Graph API and caches the result
Performance Benefits
Before Caching (Direct Graph API)
- Connection time: 30-60 seconds for 250 objects
- API calls: 50-100 requests per connection
- Network dependency: Must have internet connectivity
With Caching
- Connection time: 2-3 seconds (cache hit)
- API calls: 0 requests (cache hit)
- Offline capability: View cached data without internet
Cache Data Structure
CacheEntry Schema
Each cache entry in LiteDB has:Polymorphic Serialization
Graph API models are polymorphic (e.g.,DeviceCompliancePolicy has subclasses like Windows10CompliancePolicy, IosCompliancePolicy).
The cache preserves polymorphic types by:
- Serializing each object with its runtime type (not base type)
- Storing OData type discriminator (
@odata.type) in JSON - On deserialization, resolving the OData type to the correct C# class
Windows10CompliancePolicy.PasswordRequired) are preserved.
Multi-Tenant Cache
Cache is per-tenant, keyed by Tenant ID:- Tenant A’s cache is isolated from Tenant B’s cache
- Switching tenants does not invalidate cache for other tenants
- Each tenant has its own 24-hour TTL
- Connect to Tenant A at 9:00 AM → Cache expires at 9:00 AM next day
- Connect to Tenant B at 11:00 AM → Cache expires at 11:00 AM next day
- Reconnect to Tenant A at 3:00 PM (same day) → Load from cache (not expired)
Cache Troubleshooting
Data seems stale
Cause: Cache is fresh (< 24 hours old) but tenant data changed Solution: Click Refresh to bypass cache and fetch fresh data.”Failed to load from cache” error
Cause: Cache corruption or schema change Solution: Delete%LocalAppData%\Intune.Commander\cache.db and cache-key.bin. Reconnect to rebuild cache.
Cache database growing large
Cause: Many tenants or large object counts Solution: LiteDB is efficient, but if needed, deletecache.db to start fresh. Expired entries are cleaned automatically over time.
DataProtection key error
Cause: Windows user profile rebuilt, DataProtection keys lost Solution: Deletecache.db and cache-key.bin. The app regenerates both on next run.