|
| 1 | +--- |
| 2 | +title: Central Metadata Cache for Policy Fragments |
| 3 | +titleSuffix: Azure API Management |
| 4 | +description: Implementation guidance for shared metadata caching pattern across policy fragments in Azure API Management. |
| 5 | +services: api-management |
| 6 | +author: nicolela |
| 7 | + |
| 8 | +ms.service: azure-api-management |
| 9 | +ms.topic: concept-article |
| 10 | +ms.date: 02/10/2026 |
| 11 | +ms.author: nicolela |
| 12 | +--- |
| 13 | + |
| 14 | +# Central metadata cache for policy fragments |
| 15 | + |
| 16 | +[!INCLUDE [api-management-availability-all-tiers](../../includes/api-management-availability-all-tiers.md)] |
| 17 | + |
| 18 | +When multiple policy fragments need access to shared metadata such as common configuration data, use a cross-request caching approach to optimize performance. Rather than parsing metadata repeatedly in each fragment, a parse-once, cache-everywhere approach dramatically improves performance while ensuring data consistency. With this approach, metadata is **parsed once** on the first request when the cache is empty, then **retrieved from the cache** for all subsequent requests until the cache expires or the cache version changes. |
| 19 | + |
| 20 | +## Recommended approach |
| 21 | + |
| 22 | +This approach requires two fragments: one for storing shared metadata and another for parsing and caching the metadata. |
| 23 | + |
| 24 | +### 1. Metadata fragment |
| 25 | + |
| 26 | +The metadata fragment serves as the single source of truth for shared metadata accessed by other fragments in the pipeline: |
| 27 | +- **Centralized JSON storage**: Stores all metadata as JSON. |
| 28 | +- **Cache settings**: Includes cache settings with versioning and duration (Time to Live, or TTL). |
| 29 | + |
| 30 | +### 2. Parsing and caching fragment |
| 31 | + |
| 32 | +The parsing and caching fragment implements the following behaviors: |
| 33 | + |
| 34 | +- **Single parse operation**: Uses `JObject.Parse()` to parse the JSON stored in the metadata fragment once at the start of each pipeline request if the cache is empty. |
| 35 | +- **Cross-request caching**: Stores and retrieves parsed metadata sections as a `JObject` using the built-in [cache-store-value](cache-store-value-policy.md) and [cache-lookup-value](cache-lookup-value-policy.md) policies across multiple requests. |
| 36 | +- **Cache-first access**: Subsequent requests retrieve a parsed `JObject` directly from the cache, providing immediate access to all fragments without reparsing. |
| 37 | +- **Cache invalidation**: Cache refreshes when the metadata version changes or the cache duration (TTL) expires. |
| 38 | + |
| 39 | +## Implementation details |
| 40 | + |
| 41 | +To implement this pattern, insert both fragments into a product or API policy definition at the beginning of the inbound phase. The metadata fragment must be inserted first, followed by the parsing and caching fragment. For example: |
| 42 | + |
| 43 | +```xml |
| 44 | +<policies> |
| 45 | + <inbound> |
| 46 | + <base /> |
| 47 | + <include-fragment fragment-id="metadata-fragment" /> |
| 48 | + <include-fragment fragment-id="parse-cache-fragment" /> |
| 49 | + </inbound> |
| 50 | +</policies> |
| 51 | +``` |
| 52 | + |
| 53 | +### Metadata fragment example |
| 54 | + |
| 55 | +The `metadata-fragment.xml` fragment stores shared JSON metadata in a [context variables](api-management-policy-expressions.md#ContextVariables) named `metadata-config`: |
| 56 | + |
| 57 | +```xml |
| 58 | +<!-- Single source of truth for all shared metadata --> |
| 59 | +<fragment fragment-id="metadata-fragment"> |
| 60 | + <set-variable name="metadata-config" value="@{return @"{ |
| 61 | + 'cache-settings': { |
| 62 | + 'config-version': '1.0', |
| 63 | + 'ttl-seconds': 3600, |
| 64 | + 'feature-flags': { |
| 65 | + 'enable-cross-request-cache': true, |
| 66 | + 'cache-bypass-header': 'X-Config-Cache-Bypass' |
| 67 | + } |
| 68 | + }, |
| 69 | + 'logging': { |
| 70 | + 'level': 'INFO', |
| 71 | + 'enabled': true |
| 72 | + }, |
| 73 | + 'rate-limits': { |
| 74 | + 'premium': { 'requests-per-minute': 1000 }, |
| 75 | + 'standard': { 'requests-per-minute': 100 }, |
| 76 | + 'basic': { 'requests-per-minute': 20 } |
| 77 | + } |
| 78 | + }";}" /> |
| 79 | +</fragment> |
| 80 | +``` |
| 81 | + |
| 82 | +### Parsing and caching fragment example |
| 83 | + |
| 84 | +The `parse-cache-fragment.xml` fragment parses the JSON stored in the `metadata-config` context variable once and provides access to the resulting `JObject`. The `metadata-config` variable must already be set by `metadata-fragment.xml`: |
| 85 | + |
| 86 | +```xml |
| 87 | +<fragment fragment-id="parse-cache-fragment"> |
| 88 | + <!-- Extract cache settings from metadata-config to determine cache version and TTL --> |
| 89 | + <set-variable name="cache-config-temp" value="@{ |
| 90 | + try { |
| 91 | + var configStr = context.Variables.GetValueOrDefault<string>("metadata-config", "{}"); |
| 92 | + if (string.IsNullOrEmpty(configStr) || configStr == "{}") { |
| 93 | + return "{\"version\":\"1.0\",\"enabled\":true,\"ttl\":3600}"; |
| 94 | + } |
| 95 | + |
| 96 | + var tempConfig = JObject.Parse(configStr); |
| 97 | + var cacheSettings = tempConfig["cache-settings"] as JObject; |
| 98 | + |
| 99 | + var result = new JObject(); |
| 100 | + result["version"] = cacheSettings?["config-version"]?.ToString() ?? "1.0"; |
| 101 | + result["enabled"] = cacheSettings?["feature-flags"]?["enable-cross-request-cache"]?.Value<bool>() ?? true; |
| 102 | + result["ttl"] = cacheSettings?["ttl-seconds"]?.Value<int>() ?? 3600; |
| 103 | + |
| 104 | + return result.ToString(Newtonsoft.Json.Formatting.None); |
| 105 | + } catch { |
| 106 | + return "{\"version\":\"1.0\",\"enabled\":true,\"ttl\":3600}"; |
| 107 | + } |
| 108 | + }" /> |
| 109 | + |
| 110 | + <!-- Parse cache configuration --> |
| 111 | + <set-variable name="cache-settings-parsed" value="@{ |
| 112 | + return JObject.Parse(context.Variables.GetValueOrDefault<string>("cache-config-temp", "{}")); |
| 113 | + }" /> |
| 114 | + |
| 115 | + <!-- Build cache key with version from cache settings --> |
| 116 | + <set-variable name="cache-key" value="@{ |
| 117 | + var settings = context.Variables.GetValueOrDefault<JObject>("cache-settings-parsed"); |
| 118 | + var version = settings?["version"]?.ToString() ?? "1.0"; |
| 119 | + return "metadata-config-parsed-v" + version; |
| 120 | + }" /> |
| 121 | + |
| 122 | + <!-- Try to get from APIM cache --> |
| 123 | + <cache-lookup-value key="@(context.Variables.GetValueOrDefault<string>("cache-key"))" variable-name="cached-config" /> |
| 124 | + |
| 125 | + <choose> |
| 126 | + <when condition="@(context.Variables.ContainsKey("cached-config"))"> |
| 127 | + <!-- Cache found - Use cached configuration --> |
| 128 | + <set-variable name="config-cache-result" value="@(true)" /> |
| 129 | + |
| 130 | + <!-- Restore cached config-parsed --> |
| 131 | + <set-variable name="config-parsed" value="@(context.Variables.GetValueOrDefault<JObject>("cached-config"))" /> |
| 132 | + |
| 133 | + <!-- Extract sections from cached metadata JObject --> |
| 134 | + <set-variable name="config-logging" value="@{ |
| 135 | + var config = context.Variables.GetValueOrDefault<JObject>("config-parsed"); |
| 136 | + return config["logging"] as JObject ?? new JObject(); |
| 137 | + }" /> |
| 138 | + |
| 139 | + <set-variable name="config-rate-limits" value="@{ |
| 140 | + var config = context.Variables.GetValueOrDefault<JObject>("config-parsed"); |
| 141 | + return config["rate-limits"] as JObject ?? new JObject(); |
| 142 | + }" /> |
| 143 | + </when> |
| 144 | + <otherwise> |
| 145 | + <!-- Cache miss - Parse and store in cache --> |
| 146 | + <set-variable name="config-cache-result" value="@(false)" /> |
| 147 | + |
| 148 | + <!-- Parse metadata-config JSON --> |
| 149 | + <set-variable name="config-parsed" value="@{ |
| 150 | + var configStr = context.Variables.GetValueOrDefault<string>("metadata-config", "{}"); |
| 151 | + return JObject.Parse(configStr); |
| 152 | + }" /> |
| 153 | + |
| 154 | + <!-- Extract commonly used sections for direct access --> |
| 155 | + <set-variable name="config-logging" value="@{ |
| 156 | + var config = context.Variables.GetValueOrDefault<JObject>("config-parsed"); |
| 157 | + return config["logging"] as JObject ?? new JObject(); |
| 158 | + }" /> |
| 159 | + |
| 160 | + <set-variable name="config-rate-limits" value="@{ |
| 161 | + var config = context.Variables.GetValueOrDefault<JObject>("config-parsed"); |
| 162 | + return config["rate-limits"] as JObject ?? new JObject(); |
| 163 | + }" /> |
| 164 | + |
| 165 | + <!-- Store parsed metadata JObject in cache --> |
| 166 | + <cache-store-value key="@(context.Variables.GetValueOrDefault<string>("cache-key"))" |
| 167 | + value="@(context.Variables.GetValueOrDefault<JObject>("config-parsed"))" |
| 168 | + duration="@(context.Variables.GetValueOrDefault<JObject>("cache-settings-parsed")?["ttl"]?.Value<int>() ?? 3600)" /> |
| 169 | + </otherwise> |
| 170 | + </choose> |
| 171 | +</fragment> |
| 172 | +``` |
| 173 | + |
| 174 | +### Using metadata in other fragments |
| 175 | + |
| 176 | +Other fragments can now access parsed metadata sections directly. For example: |
| 177 | + |
| 178 | +```xml |
| 179 | +<fragment fragment-id="request-logging-fragment"> |
| 180 | + <!-- Access logging metadata JObject without reparsing --> |
| 181 | + <set-variable name="config-logging" value="@{ |
| 182 | + return context.Variables.GetValueOrDefault<JObject>("config-logging", new JObject()); |
| 183 | + }" /> |
| 184 | +</fragment> |
| 185 | +``` |
| 186 | + |
| 187 | +### Cache settings and invalidation |
| 188 | + |
| 189 | +The `parse-cache-fragment.xml` fragment uses the cache settings stored in the `metadata-fragment.xml` fragment to determine caching behavior and invalidation. For example, the settings can be changed as follows: |
| 190 | + |
| 191 | +```xml |
| 192 | +<!-- Example: Updated cache settings in the metadata fragment --> |
| 193 | +'cache-settings': { |
| 194 | + 'config-version': '1.0.1', <!-- Change version to invalidate cache --> |
| 195 | + 'ttl-seconds': 7200, <!-- Increase TTL to 2 hours --> |
| 196 | + 'feature-flags': { |
| 197 | + 'enable-cross-request-cache': true, |
| 198 | + 'cache-bypass-header': 'X-Config-Cache-Bypass' |
| 199 | + } |
| 200 | +} |
| 201 | +``` |
| 202 | + |
| 203 | +**How cache invalidation works:** The `parse-cache-fragment.xml` fragment constructs cache keys using the `config-version` value (for example, `metadata-config-v1.0.1`). When the version is changed to `1.0.2`, a new cache key is created (`metadata-config-v1.0.2`). Since no cached data exists for the new key, the fragment parses fresh metadata JSON. |
| 204 | + |
| 205 | +**To force cache refresh:** Update the `config-version` in the `metadata-fragment.xml` fragment. Because the cache settings are parsed on every request before the cache lookup occurs, changes to the cache configuration take effect immediately. |
| 206 | + |
| 207 | +## Testing and debugging |
| 208 | + |
| 209 | +### Cache result tracking |
| 210 | + |
| 211 | +The `parse-cache-fragment.xml` fragment sets a `config-cache-result` variable. This variable is useful for logging and in response headers for debugging: |
| 212 | + |
| 213 | +```xml |
| 214 | +<!-- Add cache status to response headers for debugging --> |
| 215 | +<set-header name="X-Config-Cache-Result" exists-action="override"> |
| 216 | + <value>@(context.Variables.GetValueOrDefault<bool>("config-cache-result", false).ToString())</value> |
| 217 | +</set-header> |
| 218 | +``` |
| 219 | + |
| 220 | +### Cache bypass |
| 221 | + |
| 222 | +To disable caching, use the cache bypass header: |
| 223 | + |
| 224 | +```bash |
| 225 | +curl -H "X-Config-Cache-Bypass: true" https://your-gateway.com/api |
| 226 | +``` |
| 227 | + |
| 228 | +The `parse-cache-fragment.xml` fragment checks for the bypass header after parsing cache settings: |
| 229 | + |
| 230 | +```xml |
| 231 | +<!-- Check if cache bypass is requested --> |
| 232 | +<set-variable name="cache-bypass-requested" value="@{ |
| 233 | + var settings = context.Variables.GetValueOrDefault<JObject>("cache-settings-parsed"); |
| 234 | + var bypassHeader = settings?["bypass-header"]?.ToString() ?? "X-Config-Cache-Bypass"; |
| 235 | + return context.Request.Headers.GetValueOrDefault(bypassHeader, "").ToLower() == "true"; |
| 236 | +}" /> |
| 237 | +``` |
| 238 | + |
| 239 | +The bypass check is then used in the caching decision logic: |
| 240 | + |
| 241 | +```xml |
| 242 | +<when condition="@{ |
| 243 | + var settings = context.Variables.GetValueOrDefault<JObject>("cache-settings-parsed"); |
| 244 | + var enabled = settings?["enabled"]?.Value<bool>() ?? false; |
| 245 | + var bypass = context.Variables.GetValueOrDefault<bool>("cache-bypass-requested", false); |
| 246 | + return enabled && !bypass; |
| 247 | +}"> |
| 248 | + <!-- Cross-request caching is enabled and not bypassed --> |
| 249 | +</when> |
| 250 | +``` |
| 251 | + |
| 252 | +## Best practices |
| 253 | + |
| 254 | +### Handle JSON parsing failures with error logging and defaults |
| 255 | + |
| 256 | +Implement error handling for JSON parsing operations to prevent fragment failures and provide fallback behavior. Wrap `JObject.Parse()` operations in try-catch blocks with meaningful default values: |
| 257 | + |
| 258 | +```xml |
| 259 | +<set-variable name="config-parsed" value="@{ |
| 260 | + try { |
| 261 | + var configJson = context.Variables.GetValueOrDefault<string>("metadata-config", "{}"); |
| 262 | + return JObject.Parse(configJson); |
| 263 | + } catch (Exception ex) { |
| 264 | + // Return default configuration on parse failure |
| 265 | + return JObject.Parse(@"{ |
| 266 | + 'logging': { 'level': 'ERROR', 'enabled': false }, |
| 267 | + 'rate-limits': { 'default': { 'requests-per-minute': 10 } } |
| 268 | + }"); |
| 269 | + } |
| 270 | +}" /> |
| 271 | + |
| 272 | +<!-- Log parse error using trace policy --> |
| 273 | +<choose> |
| 274 | + <when condition="@(context.Variables.ContainsKey("parse-error"))"> |
| 275 | + <trace source="config-parse" severity="error"> |
| 276 | + <message>@("JSON parse failed: " + context.Variables.GetValueOrDefault<string>("parse-error"))</message> |
| 277 | + </trace> |
| 278 | + </when> |
| 279 | +</choose> |
| 280 | +``` |
| 281 | + |
| 282 | +## Related content |
| 283 | + |
| 284 | +- **[Architecture for building advanced execution pipelines with policy fragments](fragment-pipeline-architecture.md)** - Foundational patterns for designing modular, scalable policy fragment architectures with clear separation of concerns. |
| 285 | +- **[Variable management for policy fragments](fragment-variable-management.md)** - Comprehensive guidance on context variable handling, safe access patterns, and inter-fragment communication. |
| 286 | +- **[Policy injection and coordination with fragments](fragment-policy-coordination.md)** - Fragment injection patterns and coordination between product and API policies. |
| 287 | +- **[Custom caching in Azure API Management](api-management-sample-cache-by-key.md)** - Learn how to cache items by key and modify the key using request headers. |
0 commit comments