9 Response Caching
When developing applications with bcnav, you often make the same API calls repeatedly—
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—
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 . ...))))) |
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—
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!
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—
(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—
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
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
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