Skip to content

Commit 2da7963

Browse files
Inherit default values for EntityCacheOption.Enabled and EntityCacheOption.Level from RuntimeCacheOptions (#3155)
## Why make this change? Closes #3149 ## What is this change? Instead of having static default values for the `EntityCacheOptions.Enabled` and `EntityCacheOptions.Level`, we inherit these default values from the `RuntimeCacheOptions`. ## How was this tested? We add a test that validates the new behavior in `CachingConfigProcessingTests` --------- Co-authored-by: Aniruddh Munde <[email protected]>
1 parent 26db37e commit 2da7963

14 files changed

Lines changed: 391 additions & 40 deletions

src/Cli.Tests/ModuleInitializer.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ public static void Init()
6767
VerifierSettings.IgnoreMember<Entity>(entity => entity.IsLinkingEntity);
6868
// Ignore the UserProvidedTtlOptions. They aren't serialized to our config file, enforced by EntityCacheOptionsConverter.
6969
VerifierSettings.IgnoreMember<EntityCacheOptions>(cacheOptions => cacheOptions.UserProvidedTtlOptions);
70+
// Ignore the UserProvidedEnabledOptions. They aren't serialized to our config file, enforced by EntityCacheOptionsConverter.
71+
VerifierSettings.IgnoreMember<EntityCacheOptions>(cacheOptions => cacheOptions.UserProvidedEnabledOptions);
7072
// Ignore the UserProvidedCustomToolEnabled. They aren't serialized to our config file, enforced by EntityMcpOptionsConverterFactory.
7173
VerifierSettings.IgnoreMember<EntityMcpOptions>(mcpOptions => mcpOptions.UserProvidedCustomToolEnabled);
7274
// Ignore the UserProvidedDmlToolsEnabled. They aren't serialized to our config file, enforced by EntityMcpOptionsConverterFactory.

src/Cli.Tests/Snapshots/AddEntityTests.AddEntityWithCachingEnabled.verified.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@
5050
Cache: {
5151
Enabled: true,
5252
TtlSeconds: 1,
53-
Level: L1L2,
53+
Level: L1,
5454
UserProvidedLevelOptions: false
5555
}
5656
}

src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateEntityCaching.verified.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@
4444
Cache: {
4545
Enabled: true,
4646
TtlSeconds: 1,
47-
Level: L1L2,
47+
Level: L1,
4848
UserProvidedLevelOptions: false
4949
}
5050
}

src/Config/Converters/EntityCacheOptionsConverterFactory.cs

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,9 @@ public EntityCacheOptionsConverter(DeserializationVariableReplacementSettings? r
5555
{
5656
if (reader.TokenType is JsonTokenType.StartObject)
5757
{
58-
bool? enabled = false;
58+
// Default to null (unset) so that an empty cache object ("cache": {})
59+
// is treated as "not explicitly configured" and inherits from the runtime setting.
60+
bool? enabled = null;
5961

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

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

133141
if (value?.UserProvidedTtlOptions is true)
134142
{

src/Config/ObjectModel/Entity.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT License.
33

4-
using System.Diagnostics.CodeAnalysis;
54
using System.Text.Json.Serialization;
65
using Azure.DataApiBuilder.Config.Converters;
76
using Azure.DataApiBuilder.Config.HealthCheck;
@@ -76,12 +75,13 @@ public Entity(
7675
}
7776

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

8787
[JsonIgnore]
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT License.
33

4+
using System.Runtime.Serialization;
5+
46
namespace Azure.DataApiBuilder.Config.ObjectModel;
57

68
public enum EntityCacheLevel
79
{
10+
[EnumMember(Value = "L1")]
811
L1,
12+
[EnumMember(Value = "L1L2")]
913
L1L2
1014
}

src/Config/ObjectModel/EntityCacheOptions.cs

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,12 @@ public record EntityCacheOptions
2020

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

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

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

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

4352
/// <summary>
4453
/// The cache levels to use for a cache entry.
4554
/// </summary>
4655
[JsonPropertyName("level")]
47-
public EntityCacheLevel? Level { get; init; } = null;
56+
public EntityCacheLevel? Level { get; init; }
4857

4958
[JsonConstructor]
5059
public EntityCacheOptions(bool? Enabled = null, int? TtlSeconds = null, EntityCacheLevel? Level = null)
5160
{
52-
// TODO: shouldn't we apply the same "UserProvidedXyz" logic to Enabled, too?
53-
this.Enabled = Enabled;
61+
if (Enabled is not null)
62+
{
63+
this.Enabled = Enabled;
64+
UserProvidedEnabledOptions = true;
65+
}
66+
else
67+
{
68+
this.Enabled = null;
69+
}
5470

5571
if (TtlSeconds is not null)
5672
{
@@ -73,6 +89,18 @@ public EntityCacheOptions(bool? Enabled = null, int? TtlSeconds = null, EntityCa
7389
}
7490
}
7591

92+
/// <summary>
93+
/// Flag which informs CLI and JSON serializer whether to write the enabled
94+
/// property and value to the runtime config file.
95+
/// When the user doesn't provide the enabled property/value, which signals DAB
96+
/// to inherit from the runtime cache setting, the DAB CLI should not write the
97+
/// inherited value to a serialized config. This preserves the user's intent to
98+
/// inherit rather than explicitly set the value.
99+
/// </summary>
100+
[JsonIgnore(Condition = JsonIgnoreCondition.Always)]
101+
[MemberNotNullWhen(true, nameof(Enabled))]
102+
public bool UserProvidedEnabledOptions { get; init; } = false;
103+
76104
/// <summary>
77105
/// Flag which informs CLI and JSON serializer whether to write ttl-seconds
78106
/// property and value to the runtime config file.

src/Config/ObjectModel/RuntimeCacheOptions.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,4 +65,12 @@ public RuntimeCacheOptions(bool? Enabled = null, int? TtlSeconds = null)
6565
[JsonIgnore(Condition = JsonIgnoreCondition.Always)]
6666
[MemberNotNullWhen(true, nameof(TtlSeconds))]
6767
public bool UserProvidedTtlOptions { get; init; } = false;
68+
69+
/// <summary>
70+
/// Infers the cache level from the Level2 configuration.
71+
/// If Level2 is enabled, the cache level is L1L2, otherwise L1.
72+
/// </summary>
73+
[JsonIgnore]
74+
public EntityCacheLevel InferredLevel =>
75+
Level2?.Enabled is true ? EntityCacheLevel.L1L2 : EntityCacheLevel.L1;
6876
}

src/Config/ObjectModel/RuntimeConfig.cs

Lines changed: 69 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -577,7 +577,33 @@ public virtual EntityCacheLevel GetEntityCacheEntryLevel(string entityName)
577577
return entityConfig.Cache.Level.Value;
578578
}
579579

580-
return EntityCacheOptions.DEFAULT_LEVEL;
580+
// GlobalCacheEntryLevel() returns null when runtime cache is not configured.
581+
// Default to L1 to match EntityCacheOptions.DEFAULT_LEVEL.
582+
return GlobalCacheEntryLevel() ?? EntityCacheOptions.DEFAULT_LEVEL;
583+
}
584+
585+
/// <summary>
586+
/// Returns the ttl-seconds value for the global cache entry.
587+
/// If no value is explicitly set, returns the global default value.
588+
/// </summary>
589+
/// <returns>Number of seconds a cache entry should be valid before cache eviction.</returns>
590+
public virtual int GlobalCacheEntryTtl()
591+
{
592+
return Runtime is not null && Runtime.IsCachingEnabled && Runtime.Cache.UserProvidedTtlOptions
593+
? Runtime.Cache.TtlSeconds.Value
594+
: EntityCacheOptions.DEFAULT_TTL_SECONDS;
595+
}
596+
597+
/// <summary>
598+
/// Returns the cache level value for the global cache entry.
599+
/// The level is inferred from the runtime cache Level2 configuration:
600+
/// if Level2 is enabled, the level is L1L2; otherwise L1.
601+
/// Returns null when runtime cache is not configured.
602+
/// </summary>
603+
/// <returns>Cache level for a cache entry, or null if runtime cache is not configured.</returns>
604+
public virtual EntityCacheLevel? GlobalCacheEntryLevel()
605+
{
606+
return Runtime?.Cache?.InferredLevel;
581607
}
582608

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

595-
/// <summary>
596-
/// Returns the ttl-seconds value for the global cache entry.
597-
/// If no value is explicitly set, returns the global default value.
598-
/// </summary>
599-
/// <returns>Number of seconds a cache entry should be valid before cache eviction.</returns>
600-
public int GlobalCacheEntryTtl()
601-
{
602-
return Runtime is not null && Runtime.IsCachingEnabled && Runtime.Cache.UserProvidedTtlOptions
603-
? Runtime.Cache.TtlSeconds.Value
604-
: EntityCacheOptions.DEFAULT_TTL_SECONDS;
605-
}
606-
607621
private void CheckDataSourceNamePresent(string dataSourceName)
608622
{
609623
if (!_dataSourceNameToDataSource.ContainsKey(dataSourceName))
@@ -794,4 +808,46 @@ public LogLevel GetConfiguredLogLevel(string loggerFilter = "")
794808
/// </summary>
795809
[JsonIgnore]
796810
public DmlToolsConfig? McpDmlTools => Runtime?.Mcp?.DmlTools;
811+
812+
/// <summary>
813+
/// Determines whether caching is enabled for a given entity, resolving inheritance lazily.
814+
/// If the entity explicitly sets cache enabled/disabled, that value wins.
815+
/// If the entity has a cache object but did not explicitly set enabled (UserProvidedEnabledOptions is false),
816+
/// the global runtime cache enabled setting is inherited.
817+
/// If the entity has no cache config at all, the global runtime cache enabled setting is inherited.
818+
/// </summary>
819+
/// <param name="entityName">Name of the entity to check cache configuration.</param>
820+
/// <returns>Whether caching is enabled for the entity.</returns>
821+
/// <exception cref="DataApiBuilderException">Raised when an invalid entity name is provided.</exception>
822+
public virtual bool IsEntityCachingEnabled(string entityName)
823+
{
824+
if (!Entities.TryGetValue(entityName, out Entity? entityConfig))
825+
{
826+
throw new DataApiBuilderException(
827+
message: $"{entityName} is not a valid entity.",
828+
statusCode: HttpStatusCode.BadRequest,
829+
subStatusCode: DataApiBuilderException.SubStatusCodes.EntityNotFound);
830+
}
831+
832+
return IsEntityCachingEnabled(entityConfig);
833+
}
834+
835+
/// <summary>
836+
/// Determines whether caching is enabled for a given entity, resolving inheritance lazily.
837+
/// If the entity explicitly sets cache enabled/disabled (UserProvidedEnabledOptions is true), that value wins.
838+
/// Otherwise, inherits the global runtime cache enabled setting.
839+
/// </summary>
840+
/// <param name="entity">The entity to check cache configuration.</param>
841+
/// <returns>Whether caching is enabled for the entity.</returns>
842+
private bool IsEntityCachingEnabled(Entity entity)
843+
{
844+
// If entity has an explicit cache config with user-provided enabled value, use it.
845+
if (entity.Cache is not null && entity.Cache.UserProvidedEnabledOptions)
846+
{
847+
return entity.IsCachingEnabled;
848+
}
849+
850+
// Otherwise, inherit from the global runtime cache setting.
851+
return IsCachingEnabled;
852+
}
797853
}

src/Core/Resolvers/CosmosQueryEngine.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ public async Task<Tuple<JsonDocument, IMetadata>> ExecuteAsync(
9292

9393
JObject executeQueryResult = null;
9494

95-
if (runtimeConfig.CanUseCache() && runtimeConfig.Entities[structure.EntityName].IsCachingEnabled)
95+
if (runtimeConfig.CanUseCache() && runtimeConfig.IsEntityCachingEnabled(structure.EntityName))
9696
{
9797
StringBuilder dataSourceKey = new(dataSourceName);
9898

0 commit comments

Comments
 (0)