Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
340ef73
take default for entity cache enabled and level from runtime cache se…
aaronburtle Feb 24, 2026
44c8335
include test for new behavior
aaronburtle Feb 24, 2026
eea3044
addressing comments
aaronburtle Feb 26, 2026
5649f2b
merge conflicts
aaronburtle Feb 26, 2026
07a2bd9
revert accidental change
aaronburtle Feb 26, 2026
6039a1e
addressing comments
aaronburtle Feb 26, 2026
d808b30
cleanup
aaronburtle Feb 26, 2026
2fdb41b
return null for calarity when no cache
aaronburtle Mar 5, 2026
08047f1
remove duplicated function and refactor around
aaronburtle Mar 5, 2026
9eabaac
Merge branch 'main' into dev/aaronburtle/InheritEntityCacheEnabldedAn…
aaronburtle Mar 5, 2026
3baf002
remove redundant row in test
aaronburtle Mar 5, 2026
bc9cc4b
fix casing for level
aaronburtle Mar 5, 2026
88bc81e
dont write back non user provided
aaronburtle Mar 6, 2026
fbe4245
Merge branch 'main' into dev/aaronburtle/InheritEntityCacheEnabldedAn…
aaronburtle Mar 6, 2026
1ac1899
fixing tests
aaronburtle Mar 6, 2026
48fc491
Merge branch 'dev/aaronburtle/InheritEntityCacheEnabldedAndLevelFromR…
aaronburtle Mar 6, 2026
bd54ac6
Merge branch 'main' into dev/aaronburtle/InheritEntityCacheEnabldedAn…
aaronburtle Mar 6, 2026
2a80f14
fix null write back behaviors
aaronburtle Mar 6, 2026
f46687d
fix bool name
aaronburtle Mar 6, 2026
6e219c3
typo in snapshot files
aaronburtle Mar 7, 2026
f455f14
access time, L1L2 default, test added
aaronburtle Mar 9, 2026
d8399f1
resolve merge conflicts
aaronburtle Mar 9, 2026
99fc1ee
fix mocked class
aaronburtle Mar 9, 2026
45e2759
Merge branch 'main' into dev/aaronburtle/InheritEntityCacheEnabldedAn…
aaronburtle Mar 9, 2026
bebdbe8
try to fix nuget job
aaronburtle Mar 10, 2026
d727dbc
pipeline fix ported int
aaronburtle Mar 10, 2026
cc0a2b8
Merge branch 'main' into dev/aaronburtle/InheritEntityCacheEnabldedAn…
aaronburtle Mar 10, 2026
893268c
use IsCachingEnabled
aaronburtle Mar 11, 2026
f031b8b
align default level to infferred/L1
aaronburtle Mar 11, 2026
92e127b
Merge branch 'dev/aaronburtle/InheritEntityCacheEnabldedAndLevelFromR…
aaronburtle Mar 11, 2026
cbffd20
update comment
aaronburtle Mar 11, 2026
3a3fe09
fix snapshots
aaronburtle Mar 11, 2026
b1fcde4
Merge branch 'main' into dev/aaronburtle/InheritEntityCacheEnabldedAn…
Aniruddh25 Mar 11, 2026
62dd15d
Merge branch 'main' into dev/aaronburtle/InheritEntityCacheEnabldedAn…
aaronburtle Mar 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/Cli.Tests/ModuleInitializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ public static void Init()
VerifierSettings.IgnoreMember<Entity>(entity => entity.IsLinkingEntity);
// Ignore the UserProvidedTtlOptions. They aren't serialized to our config file, enforced by EntityCacheOptionsConverter.
VerifierSettings.IgnoreMember<EntityCacheOptions>(cacheOptions => cacheOptions.UserProvidedTtlOptions);
// Ignore the UserProvidedEnabledOptions. They aren't serialized to our config file, enforced by EntityCacheOptionsConverter.
VerifierSettings.IgnoreMember<EntityCacheOptions>(cacheOptions => cacheOptions.UserProvidedEnabledOptions);
// Ignore the UserProvidedCustomToolEnabled. They aren't serialized to our config file, enforced by EntityMcpOptionsConverterFactory.
VerifierSettings.IgnoreMember<EntityMcpOptions>(mcpOptions => mcpOptions.UserProvidedCustomToolEnabled);
// Ignore the UserProvidedDmlToolsEnabled. They aren't serialized to our config file, enforced by EntityMcpOptionsConverterFactory.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
Cache: {
Enabled: true,
TtlSeconds: 1,
Level: L1L2,
Level: L1,
UserProvidedLevelOptions: false
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
Cache: {
Enabled: true,
TtlSeconds: 1,
Level: L1L2,
Level: L1,
UserProvidedLevelOptions: false
}
}
Expand Down
22 changes: 15 additions & 7 deletions src/Config/Converters/EntityCacheOptionsConverterFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,9 @@ public EntityCacheOptionsConverter(DeserializationVariableReplacementSettings? r
{
if (reader.TokenType is JsonTokenType.StartObject)
{
bool? enabled = false;
// Default to null (unset) so that an empty cache object ("cache": {})
// is treated as "not explicitly configured" and inherits from the runtime setting.
bool? enabled = null;

// Defer to EntityCacheOptions record definition to define default ttl value.
int? ttlSeconds = null;
Expand Down Expand Up @@ -119,16 +121,22 @@ public EntityCacheOptionsConverter(DeserializationVariableReplacementSettings? r
}

/// <summary>
/// When writing the EntityCacheOptions back to a JSON file, only write the ttl-seconds
/// and level properties and values when EntityCacheOptions.Enabled is true.
/// This avoids polluting the written JSON file with a property the user most likely
/// omitted when writing the original DAB runtime config file.
/// This Write operation is only used when a RuntimeConfig object is serialized to JSON.
/// When writing the EntityCacheOptions back to a JSON file, only write each sub-property
/// when its corresponding UserProvided* flag is true. This avoids polluting the written
/// JSON file with properties the user omitted (defaults or inherited values).
/// If the user provided a cache object (Entity.Cache is non-null), we always write the
/// object � even if it ends up empty ("cache": {}) � because the user explicitly included it.
/// Entity.Cache being null means the user never wrote a cache property, and the serializer's
/// DefaultIgnoreCondition.WhenWritingNull suppresses the "cache" key entirely.
/// </summary>
public override void Write(Utf8JsonWriter writer, EntityCacheOptions value, JsonSerializerOptions options)
{
writer.WriteStartObject();
writer.WriteBoolean("enabled", value?.Enabled ?? false);

if (value?.UserProvidedEnabledOptions is true)
{
writer.WriteBoolean("enabled", value.Enabled!.Value);
}

if (value?.UserProvidedTtlOptions is true)
{
Expand Down
10 changes: 5 additions & 5 deletions src/Config/ObjectModel/Entity.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Serialization;
using Azure.DataApiBuilder.Config.Converters;
using Azure.DataApiBuilder.Config.HealthCheck;
Expand Down Expand Up @@ -76,12 +75,13 @@ public Entity(
}

/// <summary>
/// Resolves the value of Entity.Cache property if present, default is false.
/// Caching is enabled only when explicitly set to true.
/// Resolves the value of Entity.Cache.Enabled property if present, default is false.
/// Caching is enabled only when explicitly set to true on the entity.
/// To resolve inheritance from the global runtime cache setting, use
/// RuntimeConfig.IsEntityCachingEnabled(entityName) instead.
/// </summary>
/// <returns>Whether caching is enabled for the entity.</returns>
/// <returns>Whether caching is explicitly enabled for the entity.</returns>
[JsonIgnore]
[MemberNotNullWhen(true, nameof(Cache))]
public bool IsCachingEnabled => Cache?.Enabled is true;

[JsonIgnore]
Expand Down
4 changes: 4 additions & 0 deletions src/Config/ObjectModel/EntityCacheLevel.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.Runtime.Serialization;

namespace Azure.DataApiBuilder.Config.ObjectModel;

public enum EntityCacheLevel
{
[EnumMember(Value = "L1")]
L1,
[EnumMember(Value = "L1L2")]
L1L2
}
40 changes: 34 additions & 6 deletions src/Config/ObjectModel/EntityCacheOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,12 @@ public record EntityCacheOptions

/// <summary>
/// Default cache level for an entity.
/// Placeholder cache level value used when the entity does not explicitly set a level.
/// This value is stored on the EntityCacheOptions object but is NOT used at runtime
/// for resolution — GetEntityCacheEntryLevel() falls through to GlobalCacheEntryLevel()
/// (which infers the level from the runtime Level2 configuration) when UserProvidedLevelOptions is false.
/// </summary>
public const EntityCacheLevel DEFAULT_LEVEL = EntityCacheLevel.L1L2;
public const EntityCacheLevel DEFAULT_LEVEL = EntityCacheLevel.L1;

/// <summary>
/// The L2 cache provider we support.
Expand All @@ -30,27 +34,39 @@ public record EntityCacheOptions

/// <summary>
/// Whether the cache should be used for the entity.
/// When null after deserialization, indicates the user did not explicitly set this property,
/// and the entity should inherit the runtime-level cache enabled setting.
/// After ResolveEntityCacheInheritance runs, this will hold the resolved value
/// (inherited from runtime or explicitly set by user). Use UserProvidedEnabledOptions
/// to distinguish whether the value was user-provided or inherited.
/// </summary>
[JsonPropertyName("enabled")]
public bool? Enabled { get; init; } = false;
public bool? Enabled { get; init; }

/// <summary>
/// The number of seconds a cache entry is valid before eligible for cache eviction.
/// </summary>
[JsonPropertyName("ttl-seconds")]
public int? TtlSeconds { get; init; } = null;
public int? TtlSeconds { get; init; }

/// <summary>
/// The cache levels to use for a cache entry.
/// </summary>
[JsonPropertyName("level")]
public EntityCacheLevel? Level { get; init; } = null;
public EntityCacheLevel? Level { get; init; }

[JsonConstructor]
public EntityCacheOptions(bool? Enabled = null, int? TtlSeconds = null, EntityCacheLevel? Level = null)
{
// TODO: shouldn't we apply the same "UserProvidedXyz" logic to Enabled, too?
this.Enabled = Enabled;
if (Enabled is not null)
{
this.Enabled = Enabled;
UserProvidedEnabledOptions = true;
}
else
{
this.Enabled = null;
}

if (TtlSeconds is not null)
{
Expand All @@ -73,6 +89,18 @@ public EntityCacheOptions(bool? Enabled = null, int? TtlSeconds = null, EntityCa
}
}

/// <summary>
/// Flag which informs CLI and JSON serializer whether to write the enabled
/// property and value to the runtime config file.
/// When the user doesn't provide the enabled property/value, which signals DAB
/// to inherit from the runtime cache setting, the DAB CLI should not write the
/// inherited value to a serialized config. This preserves the user's intent to
/// inherit rather than explicitly set the value.
/// </summary>
[JsonIgnore(Condition = JsonIgnoreCondition.Always)]
[MemberNotNullWhen(true, nameof(Enabled))]
public bool UserProvidedEnabledOptions { get; init; } = false;

/// <summary>
/// Flag which informs CLI and JSON serializer whether to write ttl-seconds
/// property and value to the runtime config file.
Expand Down
8 changes: 8 additions & 0 deletions src/Config/ObjectModel/RuntimeCacheOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,4 +65,12 @@ public RuntimeCacheOptions(bool? Enabled = null, int? TtlSeconds = null)
[JsonIgnore(Condition = JsonIgnoreCondition.Always)]
[MemberNotNullWhen(true, nameof(TtlSeconds))]
public bool UserProvidedTtlOptions { get; init; } = false;

/// <summary>
/// Infers the cache level from the Level2 configuration.
/// If Level2 is enabled, the cache level is L1L2, otherwise L1.
/// </summary>
[JsonIgnore]
public EntityCacheLevel InferredLevel =>
Level2?.Enabled is true ? EntityCacheLevel.L1L2 : EntityCacheLevel.L1;
}
82 changes: 69 additions & 13 deletions src/Config/ObjectModel/RuntimeConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -577,7 +577,33 @@ public virtual EntityCacheLevel GetEntityCacheEntryLevel(string entityName)
return entityConfig.Cache.Level.Value;
}

return EntityCacheOptions.DEFAULT_LEVEL;
// GlobalCacheEntryLevel() returns null when runtime cache is not configured.
// Default to L1 to match EntityCacheOptions.DEFAULT_LEVEL.
return GlobalCacheEntryLevel() ?? EntityCacheOptions.DEFAULT_LEVEL;
}

/// <summary>
/// Returns the ttl-seconds value for the global cache entry.
/// If no value is explicitly set, returns the global default value.
/// </summary>
/// <returns>Number of seconds a cache entry should be valid before cache eviction.</returns>
public virtual int GlobalCacheEntryTtl()
{
return Runtime is not null && Runtime.IsCachingEnabled && Runtime.Cache.UserProvidedTtlOptions
? Runtime.Cache.TtlSeconds.Value
: EntityCacheOptions.DEFAULT_TTL_SECONDS;
}

/// <summary>
/// Returns the cache level value for the global cache entry.
/// The level is inferred from the runtime cache Level2 configuration:
/// if Level2 is enabled, the level is L1L2; otherwise L1.
/// Returns null when runtime cache is not configured.
/// </summary>
/// <returns>Cache level for a cache entry, or null if runtime cache is not configured.</returns>
public virtual EntityCacheLevel? GlobalCacheEntryLevel()
{
return Runtime?.Cache?.InferredLevel;
}

/// <summary>
Expand All @@ -592,18 +618,6 @@ public virtual bool CanUseCache()
return IsCachingEnabled && !setSessionContextEnabled;
}

/// <summary>
/// Returns the ttl-seconds value for the global cache entry.
/// If no value is explicitly set, returns the global default value.
/// </summary>
/// <returns>Number of seconds a cache entry should be valid before cache eviction.</returns>
public int GlobalCacheEntryTtl()
{
return Runtime is not null && Runtime.IsCachingEnabled && Runtime.Cache.UserProvidedTtlOptions
? Runtime.Cache.TtlSeconds.Value
: EntityCacheOptions.DEFAULT_TTL_SECONDS;
}

private void CheckDataSourceNamePresent(string dataSourceName)
{
if (!_dataSourceNameToDataSource.ContainsKey(dataSourceName))
Expand Down Expand Up @@ -794,4 +808,46 @@ public LogLevel GetConfiguredLogLevel(string loggerFilter = "")
/// </summary>
[JsonIgnore]
public DmlToolsConfig? McpDmlTools => Runtime?.Mcp?.DmlTools;

/// <summary>
/// Determines whether caching is enabled for a given entity, resolving inheritance lazily.
/// If the entity explicitly sets cache enabled/disabled, that value wins.
/// If the entity has a cache object but did not explicitly set enabled (UserProvidedEnabledOptions is false),
/// the global runtime cache enabled setting is inherited.
/// If the entity has no cache config at all, the global runtime cache enabled setting is inherited.
/// </summary>
/// <param name="entityName">Name of the entity to check cache configuration.</param>
/// <returns>Whether caching is enabled for the entity.</returns>
/// <exception cref="DataApiBuilderException">Raised when an invalid entity name is provided.</exception>
public virtual bool IsEntityCachingEnabled(string entityName)
{
if (!Entities.TryGetValue(entityName, out Entity? entityConfig))
{
throw new DataApiBuilderException(
message: $"{entityName} is not a valid entity.",
statusCode: HttpStatusCode.BadRequest,
subStatusCode: DataApiBuilderException.SubStatusCodes.EntityNotFound);
}

return IsEntityCachingEnabled(entityConfig);
}

/// <summary>
/// Determines whether caching is enabled for a given entity, resolving inheritance lazily.
/// If the entity explicitly sets cache enabled/disabled (UserProvidedEnabledOptions is true), that value wins.
/// Otherwise, inherits the global runtime cache enabled setting.
/// </summary>
/// <param name="entity">The entity to check cache configuration.</param>
/// <returns>Whether caching is enabled for the entity.</returns>
private bool IsEntityCachingEnabled(Entity entity)
{
// If entity has an explicit cache config with user-provided enabled value, use it.
if (entity.Cache is not null && entity.Cache.UserProvidedEnabledOptions)
{
return entity.IsCachingEnabled;
}

// Otherwise, inherit from the global runtime cache setting.
return IsCachingEnabled;
}
}
2 changes: 1 addition & 1 deletion src/Core/Resolvers/CosmosQueryEngine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ public async Task<Tuple<JsonDocument, IMetadata>> ExecuteAsync(

JObject executeQueryResult = null;

if (runtimeConfig.CanUseCache() && runtimeConfig.Entities[structure.EntityName].IsCachingEnabled)
if (runtimeConfig.CanUseCache() && runtimeConfig.IsEntityCachingEnabled(structure.EntityName))
{
StringBuilder dataSourceKey = new(dataSourceName);

Expand Down
4 changes: 2 additions & 2 deletions src/Core/Resolvers/SqlQueryEngine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,7 @@ public object ResolveList(JsonElement array, ObjectField fieldSchema, ref IMetad
{
// Entity level cache behavior checks
bool dbPolicyConfigured = !string.IsNullOrEmpty(structure.DbPolicyPredicatesForOperations[EntityActionOperation.Read]);
bool entityCacheEnabled = runtimeConfig.Entities[structure.EntityName].IsCachingEnabled;
bool entityCacheEnabled = runtimeConfig.IsEntityCachingEnabled(structure.EntityName);

// If a db policy is configured for the read operation in the context of the executing role, skip the cache.
// We want to avoid caching token metadata because token metadata can change frequently and we want to avoid caching it.
Expand Down Expand Up @@ -466,7 +466,7 @@ public object ResolveList(JsonElement array, ObjectField fieldSchema, ref IMetad
if (runtimeConfig.CanUseCache())
{
// Entity level cache behavior checks
bool entityCacheEnabled = runtimeConfig.Entities[structure.EntityName].IsCachingEnabled;
bool entityCacheEnabled = runtimeConfig.IsEntityCachingEnabled(structure.EntityName);

// Stored procedures do not support nor honor runtime config defined
// authorization policies. Here, DAB only checks that the entity has
Expand Down
Loading