{"record":{"id":"3da2af16ae708981","repo":"dotnet/aspnetcore","slug":"either-absolute-or-sliding-expiration-needs-to-be","errorCode":null,"errorMessage":"Either absolute or sliding expiration needs to be provided.","messagePattern":"Either absolute or sliding expiration needs to be provided\\.","errorType":"exception","errorClass":"InvalidOperationException","httpStatus":null,"severity":"error","filePath":"src/Caching/SqlServer/src/DatabaseOperations.cs","lineNumber":395,"sourceCode":"            absoluteExpiration = utcNow.Add(options.AbsoluteExpirationRelativeToNow.Value);\n        }\n        else if (options.AbsoluteExpiration.HasValue)\n        {\n            if (options.AbsoluteExpiration.Value <= utcNow)\n            {\n                throw new InvalidOperationException(\"The absolute expiration value must be in the future.\");\n            }\n\n            absoluteExpiration = options.AbsoluteExpiration.Value;\n        }\n        return absoluteExpiration;\n    }\n\n    private static void ValidateOptions(TimeSpan? slidingExpiration, DateTimeOffset? absoluteExpiration)\n    {\n        if (!slidingExpiration.HasValue && !absoluteExpiration.HasValue)\n        {\n            throw new InvalidOperationException(\"Either absolute or sliding expiration needs \" +\n                \"to be provided.\");\n        }\n    }\n}\n","sourceCodeStart":377,"sourceCodeEnd":400,"githubUrl":"https://github.com/dotnet/aspnetcore/blob/3600ca084e9c8b5f4174fc5e747f4c52d2100806/src/Caching/SqlServer/src/DatabaseOperations.cs#L377-L400","documentation":"Thrown by the SQL Server distributed cache's ValidateOptions when neither an absolute nor a sliding expiration is supplied for a cache entry. The backing stored procedure requires at least one expiration type to schedule deletion, so the cache refuses entries with no expiration policy.","triggerScenarios":"Calling SetAsync with a DistributedCacheEntryOptions that has neither AbsoluteExpiration, AbsoluteExpirationRelativeToNow, nor SlidingExpiration set — i.e. all three are null.","commonSituations":"Default-constructing DistributedCacheEntryOptions and forgetting to set any expiration; refactoring that removed the expiration line; assuming the cache provides a default lifetime (it does not at this code path).","solutions":["Set at least one of AbsoluteExpiration, AbsoluteExpirationRelativeToNow, or SlidingExpiration on the options instance.","Prefer SlidingExpiration for session-like data or AbsoluteExpirationRelativeToNow for fixed TTL.","If you want a global default, wrap the cache and apply a default expiration before delegating to SetAsync."],"exampleFix":"// before: no expiration\nvar opts = new DistributedCacheEntryOptions();\nawait cache.SetStringAsync(key, value, opts);\n\n// after: provide a sliding expiration\nvar opts = new DistributedCacheEntryOptions { SlidingExpiration = TimeSpan.FromMinutes(30) };\nawait cache.SetStringAsync(key, value, opts);","handlingStrategy":"validation","validationCode":"if (!options.AbsoluteExpiration.HasValue && !options.AbsoluteExpirationRelativeToNow.HasValue && !options.SlidingExpiration.HasValue) { throw new InvalidOperationException(\"Set at least one expiration.\"); }","typeGuard":"static bool HasExpiration(DistributedCacheEntryOptions o) => o.AbsoluteExpiration.HasValue || o.AbsoluteExpirationRelativeToNow.HasValue || o.SlidingExpiration.HasValue;","tryCatchPattern":null,"preventionTips":["Wrap IDistributedCache and apply a default expiration policy.","Centralize cache option construction in a helper to avoid forgetting expiration.","Add a unit test asserting every Set call supplies an expiration."],"tags":["caching","sqlserver","aspnetcore","distributed-cache","configuration"],"backgroundTag":null,"analyzedSha":"3600ca084e9c8b5f4174fc5e747f4c52d2100806","analyzedAt":"2026-08-11T16:32:30.678Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}