Skip to main content
Intune Commander uses an intelligent caching layer to reduce Graph API calls and improve performance. Data is cached locally in an encrypted LiteDB database.

How Caching Works

Initial Connection

When you first connect to a tenant:
  1. The app checks if cached data exists for this tenant
  2. If cached data is found and not expired, it’s loaded instantly from disk
  3. If no cache exists or cache is expired, the app fetches from Graph API
  4. 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.):
  1. You navigate to the category for the first time
  2. The app checks the cache
  3. If cached and not expired, data loads instantly
  4. If not cached, the app fetches from Graph API and caches the result

Cache Keys

Each object type has a unique cache key:
  • DeviceConfigurations
  • CompliancePolicies
  • Applications
  • SettingsCatalog
  • ConditionalAccess
  • EndpointSecurity
  • AppAssignments
  • DynamicGroups
  • AssignedGroups
  • etc.
Cache is keyed by: {TenantId}|{DataType} Example: 12345678-1234-1234-1234-123456789012|DeviceConfigurations

Cache Storage

Database Location

Cache is stored in:
%LocalAppData%\Intune.Commander\cache.db

Encryption

The cache database uses AES encryption via LiteDB’s built-in encryption:
  1. On first run, a random 32-byte password is generated
  2. The password is encrypted via Microsoft.AspNetCore.DataProtection and stored in:
    %LocalAppData%\Intune.Commander\cache-key.bin
    
  3. DataProtection uses Windows DPAPI (user-scoped) to protect the password
  4. 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:
  1. The app detects decryption failure
  2. Both cache.db and cache-key.bin are deleted
  3. A new cache and password are created
  4. Data is re-fetched from Graph API on next connection
This is a rare scenario but handled gracefully.

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:
  1. The app checks the cache timestamp for each data type
  2. If CachedAtUtc + 24 hours < UtcNow, the entry is expired
  3. 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:
Loaded 47 device configuration(s) from cache (cached 2 hours ago)

Cache Operations

Manual Refresh

To bypass cache and force a fresh fetch from Graph API:
1

Navigate to a category

Select the object type in the navigation tree.
2

Click Refresh

Click the Refresh button in the toolbar.
3

Wait for completion

The app fetches from Graph API and updates the cache.
Refresh reloads all currently-loaded object types, not just the selected category.

Cache Invalidation

After a Refresh:
  1. Fresh data is fetched from Graph API
  2. The cache is updated with the new data
  3. The cache timestamp is reset to UtcNow
  4. TTL starts over (expires in 24 hours)
Lazy-loaded caches (App Assignments, Groups) are also invalidated during Refresh.

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)
There is no manual “Clear Cache” button—expired entries are cleaned automatically.

Cache Behavior by Scenario

Scenario: First Launch

  1. No cache exists
  2. App fetches all core types from Graph API
  3. Data is cached with 24-hour TTL
  4. Subsequent connections load instantly from cache

Scenario: Reconnect Within 24 Hours

  1. Cache exists and is not expired
  2. App loads data instantly from cache
  3. Status bar shows: “All data loaded from cache — skipping Graph refresh”
  4. No Graph API calls are made

Scenario: Reconnect After 24 Hours

  1. Cache exists but is expired
  2. App deletes expired entries
  3. Fresh data is fetched from Graph API
  4. Cache is updated with new data

Scenario: Partial Cache Hit

  1. Some data types are cached and fresh (e.g., Device Configs)
  2. Other data types are expired or missing (e.g., Compliance Policies)
  3. App loads cached data for fresh types
  4. App fetches expired/missing types from Graph API
  5. Status bar shows: “Partial cache hit (4/28) — refreshing from Graph”

Scenario: Navigate to Lazy-Loaded Category

  1. You navigate to Conditional Access (lazy-loaded)
  2. App checks if ConditionalAccess cache key exists and is fresh
  3. If yes, data loads instantly from cache
  4. 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
For frequent use, caching reduces connection time by 10-20x and eliminates most Graph API throttling issues.

Cache Data Structure

CacheEntry Schema

Each cache entry in LiteDB has:
public class CacheEntry
{
    public string Id { get; set; }                 // {TenantId}|{DataType}
    public string TenantId { get; set; }           // Tenant GUID
    public string DataType { get; set; }           // "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
}

Polymorphic Serialization

Graph API models are polymorphic (e.g., DeviceCompliancePolicy has subclasses like Windows10CompliancePolicy, IosCompliancePolicy). The cache preserves polymorphic types by:
  1. Serializing each object with its runtime type (not base type)
  2. Storing OData type discriminator (@odata.type) in JSON
  3. On deserialization, resolving the OData type to the correct C# class
This ensures derived properties (e.g., 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
Example:
  • 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, delete cache.db to start fresh. Expired entries are cleaned automatically over time.

DataProtection key error

Cause: Windows user profile rebuilt, DataProtection keys lost Solution: Delete cache.db and cache-key.bin. The app regenerates both on next run.

Build docs developers (and LLMs) love