Skip to content
16 changes: 5 additions & 11 deletions src/Config/ObjectModel/RuntimeConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -534,7 +534,7 @@ Runtime is not null && Runtime.Host is not null
/// </summary>
/// <param name="entityName">Name of the entity to check cache configuration.</param>
/// <returns>Number of seconds (ttl) that a cache entry should be valid before cache eviction.</returns>
/// <exception cref="DataApiBuilderException">Raised when an invalid entity name is provided or if the entity has caching disabled.</exception>
/// <exception cref="DataApiBuilderException">Raised when an invalid entity name is provided.</exception>
public virtual int GetEntityCacheEntryTtl(string entityName)
{
if (!Entities.TryGetValue(entityName, out Entity? entityConfig))
Expand All @@ -547,10 +547,7 @@ public virtual int GetEntityCacheEntryTtl(string entityName)

if (!entityConfig.IsCachingEnabled)
{
throw new DataApiBuilderException(
message: $"{entityName} does not have caching enabled.",
statusCode: HttpStatusCode.BadRequest,
subStatusCode: DataApiBuilderException.SubStatusCodes.NotSupported);
return GlobalCacheEntryTtl();
}

if (entityConfig.Cache.UserProvidedTtlOptions)
Expand All @@ -569,7 +566,7 @@ public virtual int GetEntityCacheEntryTtl(string entityName)
/// </summary>
/// <param name="entityName">Name of the entity to check cache configuration.</param>
/// <returns>Cache level that a cache entry should be stored in.</returns>
/// <exception cref="DataApiBuilderException">Raised when an invalid entity name is provided or if the entity has caching disabled.</exception>
/// <exception cref="DataApiBuilderException">Raised when an invalid entity name is provided.</exception>
public virtual EntityCacheLevel GetEntityCacheEntryLevel(string entityName)
{
if (!Entities.TryGetValue(entityName, out Entity? entityConfig))
Expand All @@ -582,10 +579,7 @@ public virtual EntityCacheLevel GetEntityCacheEntryLevel(string entityName)

if (!entityConfig.IsCachingEnabled)
{
throw new DataApiBuilderException(
message: $"{entityName} does not have caching enabled.",
statusCode: HttpStatusCode.BadRequest,
subStatusCode: DataApiBuilderException.SubStatusCodes.NotSupported);
return EntityCacheOptions.DEFAULT_LEVEL;
}

if (entityConfig.Cache.UserProvidedLevelOptions)
Expand All @@ -594,7 +588,7 @@ public virtual EntityCacheLevel GetEntityCacheEntryLevel(string entityName)
}
else
{
return EntityCacheLevel.L1L2;
return EntityCacheOptions.DEFAULT_LEVEL;
}
}

Expand Down
43 changes: 43 additions & 0 deletions src/Service.Tests/Caching/CachingConfigProcessingTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -416,4 +416,47 @@ private static string GetRawConfigJson(string globalCacheConfig, string entityCa

return expectedRuntimeConfigJson.ToString();
}

/// <summary>
/// Regression test: Validates that when global runtime cache is enabled but entity cache is disabled,
/// GetEntityCacheEntryTtl and GetEntityCacheEntryLevel return sensible defaults instead of throwing.
/// Previously, these methods threw a DataApiBuilderException (BadRequest/NotSupported) when the entity
/// had caching disabled, which caused 400 errors for valid requests when the global cache was enabled.
/// </summary>
/// <param name="globalCacheConfig">Global cache configuration JSON fragment.</param>
/// <param name="entityCacheConfig">Entity cache configuration JSON fragment.</param>
/// <param name="expectedTtl">Expected TTL returned by GetEntityCacheEntryTtl.</param>
/// <param name="expectedLevel">Expected cache level returned by GetEntityCacheEntryLevel.</param>
[DataRow(@",""cache"": { ""enabled"": true, ""ttl-seconds"": 10 }", @",""cache"": { ""enabled"": false }", 10, EntityCacheLevel.L1L2, DisplayName = "Global cache enabled with custom TTL, entity cache disabled: entity falls back to global TTL.")]
[DataRow(@",""cache"": { ""enabled"": true }", @",""cache"": { ""enabled"": false }", DEFAULT_CACHE_TTL_SECONDS, EntityCacheLevel.L1L2, DisplayName = "Global cache enabled with default TTL, entity cache disabled: entity falls back to default TTL.")]
[DataRow(@",""cache"": { ""enabled"": true, ""ttl-seconds"": 10 }", @"", 10, EntityCacheLevel.L1L2, DisplayName = "Global cache enabled with custom TTL, entity cache omitted: entity falls back to global TTL.")]
[DataTestMethod]
public void GetEntityCacheEntryTtlAndLevel_DoesNotThrow_WhenRuntimeCacheEnabledAndEntityCacheDisabled(
string globalCacheConfig,
string entityCacheConfig,
int expectedTtl,
EntityCacheLevel expectedLevel)
{
// Arrange
string fullConfig = GetRawConfigJson(globalCacheConfig: globalCacheConfig, entityCacheConfig: entityCacheConfig);
RuntimeConfigLoader.TryParseConfig(
json: fullConfig,
out RuntimeConfig? config,
replacementSettings: null);

Assert.IsNotNull(config, message: "Config must not be null, runtime config JSON deserialization failed.");
Assert.IsTrue(config.IsCachingEnabled, message: "Global caching should be enabled for this test scenario.");

Entity entity = config.Entities.First().Value;
Assert.IsFalse(entity.IsCachingEnabled, message: "Entity caching should be disabled for this test scenario.");

string entityName = config.Entities.First().Key;

// Act & Assert - These calls must not throw.
int actualTtl = config.GetEntityCacheEntryTtl(entityName);
EntityCacheLevel actualLevel = config.GetEntityCacheEntryLevel(entityName);

Assert.AreEqual(expected: expectedTtl, actual: actualTtl, message: "GetEntityCacheEntryTtl should return the global/default TTL when entity cache is disabled.");
Assert.AreEqual(expected: expectedLevel, actual: actualLevel, message: "GetEntityCacheEntryLevel should return the default level when entity cache is disabled.");
}
}
5 changes: 3 additions & 2 deletions src/Service.Tests/Configuration/ConfigurationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2903,7 +2903,7 @@ public async Task ValidateErrorMessageForMutationWithoutReadPermission()
}";
string queryName = "stock_by_pk";

ValidateMutationSucceededAtDbLayer(server, client, graphQLQuery, queryName, authToken, AuthorizationResolver.ROLE_AUTHENTICATED);
await ValidateMutationSucceededAtDbLayer(server, client, graphQLQuery, queryName, authToken, AuthorizationResolver.ROLE_AUTHENTICATED);
}
finally
{
Expand Down Expand Up @@ -3225,7 +3225,7 @@ public async Task ValidateInheritanceOfReadPermissionFromAnonymous()
/// <param name="query">GraphQL query/mutation text</param>
/// <param name="queryName">GraphQL query/mutation name</param>
/// <param name="authToken">Auth token for the graphQL request</param>
private static async void ValidateMutationSucceededAtDbLayer(TestServer server, HttpClient client, string query, string queryName, string authToken, string clientRoleHeader)
private static async Task ValidateMutationSucceededAtDbLayer(TestServer server, HttpClient client, string query, string queryName, string authToken, string clientRoleHeader)
{
JsonElement queryResponse = await GraphQLRequestExecutor.PostGraphQLRequestAsync(
client,
Expand All @@ -3237,6 +3237,7 @@ private static async void ValidateMutationSucceededAtDbLayer(TestServer server,
clientRoleHeader: clientRoleHeader);

Assert.IsNotNull(queryResponse);
Assert.AreNotEqual(JsonValueKind.Null, queryResponse.ValueKind, "Expected a JSON object response but received null.");
Assert.IsFalse(queryResponse.TryGetProperty("errors", out _));
}

Expand Down
Loading