On this page:
9.1 Why cache?
9.2 Enabling the cache
9.3 Cache modes
9.3.1 Manual mode (default)
9.3.2 All mode
9.4 Working with the cache API
9.4.1 Caching list endpoints
9.4.2 Caching filtered queries
9.4.3 Caching individual records
9.4.4 Removing cached data
9.5 Inspecting the cache
9.6 Creating test fixtures
9.6.1 Step 1:   Capture real data
9.6.2 Step 2:   Use cached data in tests
9.6.3 Step 3:   Manually edit fixtures (optional)
9.7 Cache behavior with the HTTP layer
9.7.1 Checking cache status in results
9.8 How the cache works
9.8.1 Path resolution
9.8.2 Automatic directory creation
9.8.3 Disk-only storage
9.9 Best practices
9.9.1 Use separate cache directories
9.9.2 Clear cache when data changes
9.9.3 Be mindful of sensitive data
9.9.4 Use parameterize for isolation
9.10 Troubleshooting
9.10.1 Cache not being used
9.10.2 Stale data
9.10.3 Cache file not found
9.3

9 Response Caching🔗

When developing applications with bcnav, you often make the same API calls repeatedly—testing a query, refining your code, restarting your program. Each call takes time and counts against API rate limits. bcnav’s caching system lets you save responses locally and replay them instantly.

9.1 Why cache?🔗

Caching API responses is valuable in several scenarios:

  • Faster development: Avoid waiting for network round-trips while iterating on code

  • Rate limit safety: Stay well under BC’s throttling limits during heavy development

  • Offline work: Continue working when disconnected from the network

  • Test fixtures: Create reproducible test data from real API responses

  • Large datasets: Cache a full entity list once, then query it locally many times

9.2 Enabling the cache🔗

Caching is controlled by two parameters:

;; Where to store cache files (required to enable caching)
(current-bc-cache-dir "/path/to/cache")
 
;; Whether to auto-save responses (optional, defaults to ’manual)
(current-bc-cache-mode 'manual)  ;; or ’all

With current-bc-cache-dir set, bcnav will check the cache before every GET request. If matching data exists, it’s returned immediately without calling the API.

9.3 Cache modes🔗

9.3.1 Manual mode (default)🔗

In manual mode, you control exactly what gets cached:

(current-bc-cache-dir "./bc-cache")
(current-bc-cache-mode 'manual)  ;; This is the default
 
;; API calls work normally (no auto-caching)
(customers-list)  ;; Calls the API
 
;; Manually cache specific data
(bc-cache-put! customers (customers-list))
 
;; Now this uses the cache
(customers-list)  ;; Returns cached data instantly

Manual mode is ideal when you want precise control over what’s cached. You might cache a customer list but not individual customer lookups, or cache production data but not sandbox queries.

9.3.2 All mode🔗

In 'all mode, every GET response is automatically saved:

(current-bc-cache-dir "./bc-cache")
(current-bc-cache-mode 'all)
 
;; First call hits the API and saves to cache
(customers-list)  ;; API call + cache write
 
;; Subsequent calls use cache
(customers-list)  ;; Instant, from cache

This is convenient for capturing a session’s worth of data. Start fresh with (cache-clear!), do your work, and all responses are saved for later replay.

9.4 Working with the cache API🔗

The cache API is entity-based—you work with bc-entity structs like customers rather than URLs.

9.4.1 Caching list endpoints🔗

;; Cache all customers
(bc-cache-put! customers (customers-list))
 
;; Retrieve from cache
(bc-cache-get customers)  ;; Returns cached data or #f

9.4.2 Caching filtered queries🔗

Different OData queries are cached separately:

(define active-query (make-query #:filter (= 'blocked #f)))
(define blocked-query (make-query #:filter (= 'blocked #t)))
 
;; Cache filtered results
(bc-cache-put! customers (customers-list #:query active-query) active-query)
(bc-cache-put! customers (customers-list #:query blocked-query) blocked-query)
 
;; Each query has its own cache entry
(bc-cache-get customers active-query)   ;; Active customers
(bc-cache-get customers blocked-query)  ;; Blocked customers
(bc-cache-get customers)                ;; #f (no unfiltered cache)

9.4.3 Caching individual records🔗

;; Cache a specific customer
(define cust-id "abc-123-def")
(bc-cache-put! customers cust-id (customers-get cust-id))
 
;; Retrieve by ID
(bc-cache-get customers cust-id)

9.4.4 Removing cached data🔗

;; Remove specific entries
(bc-cache-remove! customers)               ;; Remove list cache
(bc-cache-remove! customers some-query)    ;; Remove filtered cache
(bc-cache-remove! customers "abc-123")     ;; Remove record cache
 
;; Clear everything
(cache-clear!)

9.5 Inspecting the cache🔗

Use cache-list to see what’s cached:

> (cache-list)

'(#hasheq((url . "https://api.businesscentral.dynamics.com/.../customers")

          (query . ())

          (cached-at . 1702900000)

          (cache-file . "/path/to/cache/abc123.rktd")

          (response . #hasheq((value . ...)))))

Each entry shows:
  • The original URL and query parameters

  • When it was cached

  • The cache file path

  • The cached response data

9.6 Creating test fixtures🔗

One powerful use of caching is creating reproducible test data. Here’s a typical workflow:

9.6.1 Step 1: Capture real data🔗

;; Connect to production/sandbox
(bc-authenticate!)
 
;; Set up cache
(current-bc-cache-dir "./test/fixtures")
(current-bc-cache-mode 'all)
(cache-clear!)
 
;; Make the API calls your tests need
(customers-list)
(vendors-list)
(customers-get "specific-customer-id")
 
;; Check what was captured
(for ([entry (cache-list)])
  (displayln (hash-ref entry 'url)))

9.6.2 Step 2: Use cached data in tests🔗

;; In your test file
(parameterize ([current-bc-cache-dir "./test/fixtures"]
               [current-bc-cache-mode 'manual]
               [current-bc-tenant "test-tenant"]
               [current-bc-environment "test-env"]
               [current-bc-company "test-company"])
  ;; These return cached data without hitting the API
  (define custs (customers-list))
  (check-equal? (bc-result-count custs) 42)
  (check-true (bc-result-cached? custs))
  ...)

The cache files are portable—commit them to version control so tests run the same everywhere.

9.6.3 Step 3: Manually edit fixtures (optional)🔗

Cache files are readable ".rktd" files. You can edit them to create specific test scenarios:

;; Manually create a cache file
(define fixture-data
  '#hasheq((url . "https://api.businesscentral.dynamics.com/.../customers")
           (query)
           (cached-at . 0)
           (response . #hasheq((value . (#hasheq((id . "test-1")
                                                 (displayName . "Test Customer")
                                                 (blocked . #f))))))))
 
;; Write it to the cache directory with the correct key
(bc-cache-put! customers '#hasheq((value . (...test data...))))

9.7 Cache behavior with the HTTP layer🔗

When current-bc-cache-dir is set, caching integrates automatically with all entity functions:

(current-bc-cache-dir "./cache")
 
;; Seed the cache
(bc-cache-put! customers some-customer-data)
 
;; Now entity functions check cache first
(customers-list)  ;; Returns cached data, no API call!

This means you can:
  • Pre-populate the cache before running code that makes API calls

  • Record a session with 'all mode, then replay it offline

  • Mix cached and live data (cached entities return instantly, others call API)

9.7.1 Checking cache status in results🔗

The bc-result struct returned by list operations includes a cached? field that indicates whether the data came from the cache:

(define result (customers-list))
(bc-result-cached? result)  ;; #t if from cache, #f if from API

This is also shown in the REPL display:

> (customers-list)

#<bc-result: customers (42 records, ~125 KB, cached)>

The "cached" tag appears when the data was served from the cache.

9.8 How the cache works🔗

Understanding these implementation details helps avoid surprises:

9.8.1 Path resolution🔗

Relative paths are resolved against current-directory at the time of each cache operation—not when you set the parameter. This means:

(current-bc-cache-dir "cache")  ;; Relative path
 
;; In /home/user/project:
(bc-cache-put! customers data)  ;; Writes to /home/user/project/cache/
 
;; Later, if you cd to /home/user/other:
(bc-cache-get customers)        ;; Looks in /home/user/other/cache/ (different!)

For consistent behavior, use absolute paths:

;; Resolve once at startup
(current-bc-cache-dir (path->string
                        (build-path (current-directory) "cache")))

9.8.2 Automatic directory creation🔗

The cache directory is created automatically when you first write to it (including parent directories). You don’t need to create it manually.

9.8.3 Disk-only storage🔗

The cache is purely file-based. Each lookup reads from disk—there’s no in-memory layer. This design means:

  • Persistence: Cached data survives program restarts

  • No memory growth: Large datasets don’t consume RAM

  • Simplicity: No cache invalidation complexity

  • Trade-off: Repeated lookups within a session pay disk I/O each time (though OS filesystem caching helps)

For most development and testing scenarios, disk speed is more than adequate. If you need in-memory caching for performance-critical code, you can add your own memoization layer on top.

9.9 Best practices🔗

9.9.1 Use separate cache directories🔗

Keep different caches for different purposes:

;; Development cache (throwaway)
(current-bc-cache-dir "./tmp/dev-cache")
 
;; Test fixtures (version-controlled)
(current-bc-cache-dir "./test/fixtures")
 
;; Production data snapshot (archived)
(current-bc-cache-dir "./data/2024-01-snapshot")

9.9.2 Clear cache when data changes🔗

Cached data can become stale. Clear it when:
  • You change your tenant/environment/company configuration

  • You expect data to have changed in BC

  • You’re switching between different scenarios

9.9.3 Be mindful of sensitive data🔗

Cache files contain real API responses, which may include sensitive information. Don’t commit production cache files to public repositories.

9.9.4 Use parameterize for isolation🔗

Wrap cache usage in parameterize to avoid affecting global state:

(define (with-test-cache thunk)
  (parameterize ([current-bc-cache-dir "./test/fixtures"]
                 [current-bc-cache-mode 'manual])
    (thunk)))
 
(with-test-cache
  (lambda ()
    ;; Code here uses test fixtures
    (customers-list)))

9.10 Troubleshooting🔗

9.10.1 Cache not being used🔗

Check that:
  • current-bc-cache-dir is set to an existing directory

  • The cache file exists (use cache-list to check)

  • Your tenant/environment/company match what was used when caching

Cache keys include the full URL, which incorporates tenant, environment, and company. If these don’t match exactly, the cache won’t hit.

9.10.2 Stale data🔗

If you’re getting outdated results:

;; Check when data was cached
(for ([entry (cache-list)])
  (printf "~a cached at ~a\n"
          (hash-ref entry 'url)
          (hash-ref entry 'cached-at)))
 
;; Clear and refresh
(cache-clear!)

9.10.3 Cache file not found🔗

Entity-based functions like bc-cache-put! require current-bc-cache-dir to be set:

> (bc-cache-put! customers data)

bc-cache-put!: current-bc-cache-dir is not set

Set the cache directory first:

(current-bc-cache-dir "./cache")
(bc-cache-put! customers data)  ;; Now works