[API Proposal]: Migrate HybridCache from aspnet to runtime  #100290

Description

@mgravell

Background and motivation

Context; this is part of Epic: IDistributedCache updates in .NET 9 and Hybrid Cache API proposal

Was originally just IDistributedCache, but this issue now updated to include all of the abstract HybridCache API (but not the aspnet implementation)

HybridCache is a new cache abstraction that sits on top of IDistributedCache (L2) and IMemoryCache (L1) to provide an integrated cache experience that includes serialization, stampede protection, and a range of other features. The API has been discussed extensively as part of aspnet, most significantly in the aforementioned dotnet/aspnetcore#54647.

We would now like to kick the aspnet bits over the fence into Microsoft.Extensions.Caching.Abstractions, so that:

  • backend implementations such as Redis, SQL, etc do not need an aspnet framework reference
  • applications other than aspnet can consume the APIs
  • other non-aspnet implementations are possible (in particular, FusionCache have expressed interest)

The specific proposed API changes are laid out in #103103


The existing IDistributedCache API is based around byte[], which is wildly inefficient for anything that isn't an in-memory lookup of string to byte[] (i.e. handing back the same array each time, which is itself a bit dangerous because of array mutation).

To be 100% explicit:

  • because the byte[] needs to be right-sized, it must be allocated per usage (especially if we want defensive copies)
  • even if we knew the length, to use an array-segment efficiently we would also need to agree a recycling strategy between caller and callee
  • byte[] demands contiguous memory, which can force LOH etc

The proposal is to add non-allocating APIs, similar to those used for Output Cache in .NET 8, to avoid these allocations; this assists every other backend - Redis, SQL, SQLite, Cosmos, etc.

As an example of the impact of this, see this table (note also the second table in the same comment), where a mocked up version of the API was used to test a FASTER-based cache backend (this is a useful backend because it has very low internal overheads).

| Method | KeyLength | PayloadLength | Mean | Error | StdDev | Gen0 | Gen1 | Allocated ||--------------- |---------- |-------------- |------------:|------------:|------------:|-------:|-------:|----------:|| Get | 128 | 10240 | 576.0 ns | 9.79 ns | 5.83 ns | 0.6123 | - | 10264 B || Set | 128 | 10240 | 882.0 ns | 23.99 ns | 22.44 ns | 0.6123 | - | 10264 B || GetAsync | 128 | 10240 | 657.6 ns | 16.96 ns | 14.16 ns | 0.6189 | - | 10360 B || SetAsync | 128 | 10240 | 1,094.7 ns | 55.15 ns | 51.58 ns | 0.6123 | - | 10264 B || | | | | | | | | || GetBuffer | 128 | 10240 | 366.1 ns | 6.22 ns | 5.20 ns | - | - | - || SetBuffer | 128 | 10240 | 495.4 ns | 7.11 ns | 2.54 ns | - | - | - || GetAsyncBuffer | 128 | 10240 | 387.9 ns | 7.60 ns | 1.97 ns | 0.0014 | - | 24 B || SetAsyncBuffer | 128 | 10240 | 649.9 ns | 12.70 ns | 11.88 ns | - | - | - |

API Proposal

// add extension API for existing IDistributedCache, to avoid byte[] overheadsnamespaceMicrosoft.Extensions.Caching.Distributed;publicinterfaceIBufferDistributedCache:IDistributedCache{boolTryGet(stringkey,IBufferWriter<byte>destination);ValueTask<bool>TryGetAsync(stringkey,IBufferWriter<byte>destination,CancellationTokentoken=default);voidSet(stringkey,ReadOnlySequence<byte>value,DistributedCacheEntryOptionsoptions);ValueTaskSetAsync(stringkey,ReadOnlySequence<byte>value,DistributedCacheEntryOptionsoptions,CancellationTokentoken=default);}// define abstract API for new HybridCache systemnamespaceMicrosoft.Extensions.Caching.Hybrid;publicabstractclassHybridCache{publicabstractValueTask<T>GetOrCreateAsync<TState,T>(stringkey,TStatestate,Func<TState,CancellationToken,ValueTask<T>>factory,HybridCacheEntryOptions?options=null,IEnumerable<string>?tags=null,CancellationTokencancellationToken=default);publicValueTask<T>GetOrCreateAsync<T>(stringkey,Func<CancellationToken,ValueTask<T>>factory,HybridCacheEntryOptions?options=null,IEnumerable<string>?tags=null,CancellationTokencancellationToken=default);publicabstractValueTaskSetAsync<T>(stringkey,Tvalue,HybridCacheEntryOptions?options=null,IEnumerable<string>?tags=null,CancellationTokencancellationToken=default);publicabstractValueTaskRemoveAsync(stringkey,CancellationTokencancellationToken=default);publicvirtualValueTaskRemoveAsync(IEnumerable<string>keys,CancellationTokencancellationToken=default);publicvirtualValueTaskRemoveByTagAsync(IEnumerable<string>tags,CancellationTokencancellationToken=default);publicabstractValueTaskRemoveByTagAsync(stringtag,CancellationTokencancellationToken=default);}publicsealedclassHybridCacheEntryOptions{publicTimeSpan?Expiration{get;init;}publicTimeSpan?LocalCacheExpiration{get;init;}publicHybridCacheEntryFlags?Flags{get;init;}}[Flags]publicenumHybridCacheEntryFlags{None=0,DisableLocalCacheRead=1<<0,DisableLocalCacheWrite=1<<1,DisableLocalCache=DisableLocalCacheRead|DisableLocalCacheWrite,DisableDistributedCacheRead=1<<2,DisableDistributedCacheWrite=1<<3,DisableDistributedCache=DisableDistributedCacheRead|DisableDistributedCacheWrite,DisableUnderlyingData=1<<4,DisableCompression=1<<5,}publicinterfaceIHybridCacheSerializer<T>{TDeserialize(ReadOnlySequence<byte>source);voidSerialize(Tvalue,IBufferWriter<byte>target);}publicinterfaceIHybridCacheSerializerFactory{boolTryCreateSerializer<T>([NotNullWhen(true)]outIHybridCacheSerializer<T>?serializer);}

(edit: changed name from cancellationToken to token to mirror IDistributedCache, and added = default)
(edit: added the sync paths)

API Usage

The usage of this API is optional; existing backends that implement IDistributedCachemay choose (or not) to additionally implement the new API. The new "hybrid cache" piece will type-test for the feature, and use it appropriately. Any backends that do not implement the API: continue to work, using the byte[] allocation.

The design for "set" is simple: the caller owns the memory lifetime via ReadOnlySequence<byte> - the backend (as an API contract) is explicitly meant to copy the data out; storing the passed in value is undefined behaviour as that data may go out of scope.

The design for "get" is for the caller to handle memory management (which would otherwise be duplicated and brittle in every backend); this is achieved by passing in an IBufferWriter<byte> to which the backend can push the data. This also means that the "hybrid cache" piece can handle quotas etc before data is fully read. The bool return-value is used to distinguish "found" vs "not found"; this is null vs not null on the old API, and is necessary because zero bytes is a valid payload length in some formats (I'm looking at you, protobuf).


Note that hybrid cache proposal only uses async fetch; however, it is noted that if we only added async methods, this would mean that IBufferDistributedCache is "unbalanced" vs IDistributedCache (sync vs async), and omitting it would limit our options later if we decide to add sync paths on hybrid cache; accordingly, sync get+set paths are included in this proposal.

Alternative Designs

  • Stream - allocatey, indirect, and multi-copylicious; significant complications for producer and consumer
  • IMemoryOwner<byte> or similar return - contiguous, caller gets no chance to intercept until all prepared
  • ReadOnlySpan<byte> input (to avoid storage) - contiguous, only applies to "sync"
  • default interface methods rather than new interface - would be a similar API change, but would involve an extra memcpy and lease for each fetch (default implementation would be "get array, write array to buffer-writer, which in turn needs to lease"); it is preferable to type test instead (once at setup), and use the most efficient strategy
  • naming... yeah, I'm open to offers

Risks

None; any existing backends not implementing the feature continue to work as current, hopefully adding support in time; there is no additional service registration for this auxiliary API

Metadata

Metadata

Assignees

No one assigned

    Labels

    api-approvedAPI was approved in API review, it can be implementedarea-Extensions-CachingblockingMarks issues that we want to fast track in order to unblock other important work

    Type

    No type

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

      , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
       blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
      }
      } catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
      })();
      (function(){
      try {
      var __m = "github.com";
      var __re = new RegExp('^' + "github\\.com" + '
      
      Skip to content

      [API Proposal]: Migrate HybridCache from aspnet to runtime  #100290

      Description

      @mgravell

      Background and motivation

      Context; this is part of Epic: IDistributedCache updates in .NET 9 and Hybrid Cache API proposal

      Was originally just IDistributedCache, but this issue now updated to include all of the abstract HybridCache API (but not the aspnet implementation)

      HybridCache is a new cache abstraction that sits on top of IDistributedCache (L2) and IMemoryCache (L1) to provide an integrated cache experience that includes serialization, stampede protection, and a range of other features. The API has been discussed extensively as part of aspnet, most significantly in the aforementioned dotnet/aspnetcore#54647.

      We would now like to kick the aspnet bits over the fence into Microsoft.Extensions.Caching.Abstractions, so that:

      • backend implementations such as Redis, SQL, etc do not need an aspnet framework reference
      • applications other than aspnet can consume the APIs
      • other non-aspnet implementations are possible (in particular, FusionCache have expressed interest)

      The specific proposed API changes are laid out in #103103


      The existing IDistributedCache API is based around byte[], which is wildly inefficient for anything that isn't an in-memory lookup of string to byte[] (i.e. handing back the same array each time, which is itself a bit dangerous because of array mutation).

      To be 100% explicit:

      • because the byte[] needs to be right-sized, it must be allocated per usage (especially if we want defensive copies)
      • even if we knew the length, to use an array-segment efficiently we would also need to agree a recycling strategy between caller and callee
      • byte[] demands contiguous memory, which can force LOH etc

      The proposal is to add non-allocating APIs, similar to those used for Output Cache in .NET 8, to avoid these allocations; this assists every other backend - Redis, SQL, SQLite, Cosmos, etc.

      As an example of the impact of this, see this table (note also the second table in the same comment), where a mocked up version of the API was used to test a FASTER-based cache backend (this is a useful backend because it has very low internal overheads).

      | Method | KeyLength | PayloadLength | Mean | Error | StdDev | Gen0 | Gen1 | Allocated ||--------------- |---------- |-------------- |------------:|------------:|------------:|-------:|-------:|----------:|| Get | 128 | 10240 | 576.0 ns | 9.79 ns | 5.83 ns | 0.6123 | - | 10264 B || Set | 128 | 10240 | 882.0 ns | 23.99 ns | 22.44 ns | 0.6123 | - | 10264 B || GetAsync | 128 | 10240 | 657.6 ns | 16.96 ns | 14.16 ns | 0.6189 | - | 10360 B || SetAsync | 128 | 10240 | 1,094.7 ns | 55.15 ns | 51.58 ns | 0.6123 | - | 10264 B || | | | | | | | | || GetBuffer | 128 | 10240 | 366.1 ns | 6.22 ns | 5.20 ns | - | - | - || SetBuffer | 128 | 10240 | 495.4 ns | 7.11 ns | 2.54 ns | - | - | - || GetAsyncBuffer | 128 | 10240 | 387.9 ns | 7.60 ns | 1.97 ns | 0.0014 | - | 24 B || SetAsyncBuffer | 128 | 10240 | 649.9 ns | 12.70 ns | 11.88 ns | - | - | - |

      API Proposal

      // add extension API for existing IDistributedCache, to avoid byte[] overheadsnamespaceMicrosoft.Extensions.Caching.Distributed;publicinterfaceIBufferDistributedCache:IDistributedCache{boolTryGet(stringkey,IBufferWriter<byte>destination);ValueTask<bool>TryGetAsync(stringkey,IBufferWriter<byte>destination,CancellationTokentoken=default);voidSet(stringkey,ReadOnlySequence<byte>value,DistributedCacheEntryOptionsoptions);ValueTaskSetAsync(stringkey,ReadOnlySequence<byte>value,DistributedCacheEntryOptionsoptions,CancellationTokentoken=default);}// define abstract API for new HybridCache systemnamespaceMicrosoft.Extensions.Caching.Hybrid;publicabstractclassHybridCache{publicabstractValueTask<T>GetOrCreateAsync<TState,T>(stringkey,TStatestate,Func<TState,CancellationToken,ValueTask<T>>factory,HybridCacheEntryOptions?options=null,IEnumerable<string>?tags=null,CancellationTokencancellationToken=default);publicValueTask<T>GetOrCreateAsync<T>(stringkey,Func<CancellationToken,ValueTask<T>>factory,HybridCacheEntryOptions?options=null,IEnumerable<string>?tags=null,CancellationTokencancellationToken=default);publicabstractValueTaskSetAsync<T>(stringkey,Tvalue,HybridCacheEntryOptions?options=null,IEnumerable<string>?tags=null,CancellationTokencancellationToken=default);publicabstractValueTaskRemoveAsync(stringkey,CancellationTokencancellationToken=default);publicvirtualValueTaskRemoveAsync(IEnumerable<string>keys,CancellationTokencancellationToken=default);publicvirtualValueTaskRemoveByTagAsync(IEnumerable<string>tags,CancellationTokencancellationToken=default);publicabstractValueTaskRemoveByTagAsync(stringtag,CancellationTokencancellationToken=default);}publicsealedclassHybridCacheEntryOptions{publicTimeSpan?Expiration{get;init;}publicTimeSpan?LocalCacheExpiration{get;init;}publicHybridCacheEntryFlags?Flags{get;init;}}[Flags]publicenumHybridCacheEntryFlags{None=0,DisableLocalCacheRead=1<<0,DisableLocalCacheWrite=1<<1,DisableLocalCache=DisableLocalCacheRead|DisableLocalCacheWrite,DisableDistributedCacheRead=1<<2,DisableDistributedCacheWrite=1<<3,DisableDistributedCache=DisableDistributedCacheRead|DisableDistributedCacheWrite,DisableUnderlyingData=1<<4,DisableCompression=1<<5,}publicinterfaceIHybridCacheSerializer<T>{TDeserialize(ReadOnlySequence<byte>source);voidSerialize(Tvalue,IBufferWriter<byte>target);}publicinterfaceIHybridCacheSerializerFactory{boolTryCreateSerializer<T>([NotNullWhen(true)]outIHybridCacheSerializer<T>?serializer);}

      (edit: changed name from cancellationToken to token to mirror IDistributedCache, and added = default)
      (edit: added the sync paths)

      API Usage

      The usage of this API is optional; existing backends that implement IDistributedCachemay choose (or not) to additionally implement the new API. The new "hybrid cache" piece will type-test for the feature, and use it appropriately. Any backends that do not implement the API: continue to work, using the byte[] allocation.

      The design for "set" is simple: the caller owns the memory lifetime via ReadOnlySequence<byte> - the backend (as an API contract) is explicitly meant to copy the data out; storing the passed in value is undefined behaviour as that data may go out of scope.

      The design for "get" is for the caller to handle memory management (which would otherwise be duplicated and brittle in every backend); this is achieved by passing in an IBufferWriter<byte> to which the backend can push the data. This also means that the "hybrid cache" piece can handle quotas etc before data is fully read. The bool return-value is used to distinguish "found" vs "not found"; this is null vs not null on the old API, and is necessary because zero bytes is a valid payload length in some formats (I'm looking at you, protobuf).


      Note that hybrid cache proposal only uses async fetch; however, it is noted that if we only added async methods, this would mean that IBufferDistributedCache is "unbalanced" vs IDistributedCache (sync vs async), and omitting it would limit our options later if we decide to add sync paths on hybrid cache; accordingly, sync get+set paths are included in this proposal.

      Alternative Designs

      • Stream - allocatey, indirect, and multi-copylicious; significant complications for producer and consumer
      • IMemoryOwner<byte> or similar return - contiguous, caller gets no chance to intercept until all prepared
      • ReadOnlySpan<byte> input (to avoid storage) - contiguous, only applies to "sync"
      • default interface methods rather than new interface - would be a similar API change, but would involve an extra memcpy and lease for each fetch (default implementation would be "get array, write array to buffer-writer, which in turn needs to lease"); it is preferable to type test instead (once at setup), and use the most efficient strategy
      • naming... yeah, I'm open to offers

      Risks

      None; any existing backends not implementing the feature continue to work as current, hopefully adding support in time; there is no additional service registration for this auxiliary API

      Metadata

      Metadata

      Assignees

      No one assigned

        Labels

        api-approvedAPI was approved in API review, it can be implementedarea-Extensions-CachingblockingMarks issues that we want to fast track in order to unblock other important work

        Type

        No type

        Projects

        No projects

          Milestone

          Relationships

          None yet

          Development

          No branches or pull requests

          Issue actions

          , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
          Skip to content

          [API Proposal]: Migrate HybridCache from aspnet to runtime  #100290

          Description

          @mgravell

          Background and motivation

          Context; this is part of Epic: IDistributedCache updates in .NET 9 and Hybrid Cache API proposal

          Was originally just IDistributedCache, but this issue now updated to include all of the abstract HybridCache API (but not the aspnet implementation)

          HybridCache is a new cache abstraction that sits on top of IDistributedCache (L2) and IMemoryCache (L1) to provide an integrated cache experience that includes serialization, stampede protection, and a range of other features. The API has been discussed extensively as part of aspnet, most significantly in the aforementioned dotnet/aspnetcore#54647.

          We would now like to kick the aspnet bits over the fence into Microsoft.Extensions.Caching.Abstractions, so that:

          • backend implementations such as Redis, SQL, etc do not need an aspnet framework reference
          • applications other than aspnet can consume the APIs
          • other non-aspnet implementations are possible (in particular, FusionCache have expressed interest)

          The specific proposed API changes are laid out in #103103


          The existing IDistributedCache API is based around byte[], which is wildly inefficient for anything that isn't an in-memory lookup of string to byte[] (i.e. handing back the same array each time, which is itself a bit dangerous because of array mutation).

          To be 100% explicit:

          • because the byte[] needs to be right-sized, it must be allocated per usage (especially if we want defensive copies)
          • even if we knew the length, to use an array-segment efficiently we would also need to agree a recycling strategy between caller and callee
          • byte[] demands contiguous memory, which can force LOH etc

          The proposal is to add non-allocating APIs, similar to those used for Output Cache in .NET 8, to avoid these allocations; this assists every other backend - Redis, SQL, SQLite, Cosmos, etc.

          As an example of the impact of this, see this table (note also the second table in the same comment), where a mocked up version of the API was used to test a FASTER-based cache backend (this is a useful backend because it has very low internal overheads).

          | Method | KeyLength | PayloadLength | Mean | Error | StdDev | Gen0 | Gen1 | Allocated ||--------------- |---------- |-------------- |------------:|------------:|------------:|-------:|-------:|----------:|| Get | 128 | 10240 | 576.0 ns | 9.79 ns | 5.83 ns | 0.6123 | - | 10264 B || Set | 128 | 10240 | 882.0 ns | 23.99 ns | 22.44 ns | 0.6123 | - | 10264 B || GetAsync | 128 | 10240 | 657.6 ns | 16.96 ns | 14.16 ns | 0.6189 | - | 10360 B || SetAsync | 128 | 10240 | 1,094.7 ns | 55.15 ns | 51.58 ns | 0.6123 | - | 10264 B || | | | | | | | | || GetBuffer | 128 | 10240 | 366.1 ns | 6.22 ns | 5.20 ns | - | - | - || SetBuffer | 128 | 10240 | 495.4 ns | 7.11 ns | 2.54 ns | - | - | - || GetAsyncBuffer | 128 | 10240 | 387.9 ns | 7.60 ns | 1.97 ns | 0.0014 | - | 24 B || SetAsyncBuffer | 128 | 10240 | 649.9 ns | 12.70 ns | 11.88 ns | - | - | - |

          API Proposal

          // add extension API for existing IDistributedCache, to avoid byte[] overheadsnamespaceMicrosoft.Extensions.Caching.Distributed;publicinterfaceIBufferDistributedCache:IDistributedCache{boolTryGet(stringkey,IBufferWriter<byte>destination);ValueTask<bool>TryGetAsync(stringkey,IBufferWriter<byte>destination,CancellationTokentoken=default);voidSet(stringkey,ReadOnlySequence<byte>value,DistributedCacheEntryOptionsoptions);ValueTaskSetAsync(stringkey,ReadOnlySequence<byte>value,DistributedCacheEntryOptionsoptions,CancellationTokentoken=default);}// define abstract API for new HybridCache systemnamespaceMicrosoft.Extensions.Caching.Hybrid;publicabstractclassHybridCache{publicabstractValueTask<T>GetOrCreateAsync<TState,T>(stringkey,TStatestate,Func<TState,CancellationToken,ValueTask<T>>factory,HybridCacheEntryOptions?options=null,IEnumerable<string>?tags=null,CancellationTokencancellationToken=default);publicValueTask<T>GetOrCreateAsync<T>(stringkey,Func<CancellationToken,ValueTask<T>>factory,HybridCacheEntryOptions?options=null,IEnumerable<string>?tags=null,CancellationTokencancellationToken=default);publicabstractValueTaskSetAsync<T>(stringkey,Tvalue,HybridCacheEntryOptions?options=null,IEnumerable<string>?tags=null,CancellationTokencancellationToken=default);publicabstractValueTaskRemoveAsync(stringkey,CancellationTokencancellationToken=default);publicvirtualValueTaskRemoveAsync(IEnumerable<string>keys,CancellationTokencancellationToken=default);publicvirtualValueTaskRemoveByTagAsync(IEnumerable<string>tags,CancellationTokencancellationToken=default);publicabstractValueTaskRemoveByTagAsync(stringtag,CancellationTokencancellationToken=default);}publicsealedclassHybridCacheEntryOptions{publicTimeSpan?Expiration{get;init;}publicTimeSpan?LocalCacheExpiration{get;init;}publicHybridCacheEntryFlags?Flags{get;init;}}[Flags]publicenumHybridCacheEntryFlags{None=0,DisableLocalCacheRead=1<<0,DisableLocalCacheWrite=1<<1,DisableLocalCache=DisableLocalCacheRead|DisableLocalCacheWrite,DisableDistributedCacheRead=1<<2,DisableDistributedCacheWrite=1<<3,DisableDistributedCache=DisableDistributedCacheRead|DisableDistributedCacheWrite,DisableUnderlyingData=1<<4,DisableCompression=1<<5,}publicinterfaceIHybridCacheSerializer<T>{TDeserialize(ReadOnlySequence<byte>source);voidSerialize(Tvalue,IBufferWriter<byte>target);}publicinterfaceIHybridCacheSerializerFactory{boolTryCreateSerializer<T>([NotNullWhen(true)]outIHybridCacheSerializer<T>?serializer);}

          (edit: changed name from cancellationToken to token to mirror IDistributedCache, and added = default)
          (edit: added the sync paths)

          API Usage

          The usage of this API is optional; existing backends that implement IDistributedCachemay choose (or not) to additionally implement the new API. The new "hybrid cache" piece will type-test for the feature, and use it appropriately. Any backends that do not implement the API: continue to work, using the byte[] allocation.

          The design for "set" is simple: the caller owns the memory lifetime via ReadOnlySequence<byte> - the backend (as an API contract) is explicitly meant to copy the data out; storing the passed in value is undefined behaviour as that data may go out of scope.

          The design for "get" is for the caller to handle memory management (which would otherwise be duplicated and brittle in every backend); this is achieved by passing in an IBufferWriter<byte> to which the backend can push the data. This also means that the "hybrid cache" piece can handle quotas etc before data is fully read. The bool return-value is used to distinguish "found" vs "not found"; this is null vs not null on the old API, and is necessary because zero bytes is a valid payload length in some formats (I'm looking at you, protobuf).


          Note that hybrid cache proposal only uses async fetch; however, it is noted that if we only added async methods, this would mean that IBufferDistributedCache is "unbalanced" vs IDistributedCache (sync vs async), and omitting it would limit our options later if we decide to add sync paths on hybrid cache; accordingly, sync get+set paths are included in this proposal.

          Alternative Designs

          • Stream - allocatey, indirect, and multi-copylicious; significant complications for producer and consumer
          • IMemoryOwner<byte> or similar return - contiguous, caller gets no chance to intercept until all prepared
          • ReadOnlySpan<byte> input (to avoid storage) - contiguous, only applies to "sync"
          • default interface methods rather than new interface - would be a similar API change, but would involve an extra memcpy and lease for each fetch (default implementation would be "get array, write array to buffer-writer, which in turn needs to lease"); it is preferable to type test instead (once at setup), and use the most efficient strategy
          • naming... yeah, I'm open to offers

          Risks

          None; any existing backends not implementing the feature continue to work as current, hopefully adding support in time; there is no additional service registration for this auxiliary API

          Metadata

          Metadata

          Assignees

          No one assigned

            Labels

            api-approvedAPI was approved in API review, it can be implementedarea-Extensions-CachingblockingMarks issues that we want to fast track in order to unblock other important work

            Type

            No type

            Projects

            No projects

              Milestone

              Relationships

              None yet

              Development

              No branches or pull requests

              Issue actions

              , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
              Skip to content

              [API Proposal]: Migrate HybridCache from aspnet to runtime  #100290

              Description

              @mgravell

              Background and motivation

              Context; this is part of Epic: IDistributedCache updates in .NET 9 and Hybrid Cache API proposal

              Was originally just IDistributedCache, but this issue now updated to include all of the abstract HybridCache API (but not the aspnet implementation)

              HybridCache is a new cache abstraction that sits on top of IDistributedCache (L2) and IMemoryCache (L1) to provide an integrated cache experience that includes serialization, stampede protection, and a range of other features. The API has been discussed extensively as part of aspnet, most significantly in the aforementioned dotnet/aspnetcore#54647.

              We would now like to kick the aspnet bits over the fence into Microsoft.Extensions.Caching.Abstractions, so that:

              • backend implementations such as Redis, SQL, etc do not need an aspnet framework reference
              • applications other than aspnet can consume the APIs
              • other non-aspnet implementations are possible (in particular, FusionCache have expressed interest)

              The specific proposed API changes are laid out in #103103


              The existing IDistributedCache API is based around byte[], which is wildly inefficient for anything that isn't an in-memory lookup of string to byte[] (i.e. handing back the same array each time, which is itself a bit dangerous because of array mutation).

              To be 100% explicit:

              • because the byte[] needs to be right-sized, it must be allocated per usage (especially if we want defensive copies)
              • even if we knew the length, to use an array-segment efficiently we would also need to agree a recycling strategy between caller and callee
              • byte[] demands contiguous memory, which can force LOH etc

              The proposal is to add non-allocating APIs, similar to those used for Output Cache in .NET 8, to avoid these allocations; this assists every other backend - Redis, SQL, SQLite, Cosmos, etc.

              As an example of the impact of this, see this table (note also the second table in the same comment), where a mocked up version of the API was used to test a FASTER-based cache backend (this is a useful backend because it has very low internal overheads).

              | Method | KeyLength | PayloadLength | Mean | Error | StdDev | Gen0 | Gen1 | Allocated ||--------------- |---------- |-------------- |------------:|------------:|------------:|-------:|-------:|----------:|| Get | 128 | 10240 | 576.0 ns | 9.79 ns | 5.83 ns | 0.6123 | - | 10264 B || Set | 128 | 10240 | 882.0 ns | 23.99 ns | 22.44 ns | 0.6123 | - | 10264 B || GetAsync | 128 | 10240 | 657.6 ns | 16.96 ns | 14.16 ns | 0.6189 | - | 10360 B || SetAsync | 128 | 10240 | 1,094.7 ns | 55.15 ns | 51.58 ns | 0.6123 | - | 10264 B || | | | | | | | | || GetBuffer | 128 | 10240 | 366.1 ns | 6.22 ns | 5.20 ns | - | - | - || SetBuffer | 128 | 10240 | 495.4 ns | 7.11 ns | 2.54 ns | - | - | - || GetAsyncBuffer | 128 | 10240 | 387.9 ns | 7.60 ns | 1.97 ns | 0.0014 | - | 24 B || SetAsyncBuffer | 128 | 10240 | 649.9 ns | 12.70 ns | 11.88 ns | - | - | - |

              API Proposal

              // add extension API for existing IDistributedCache, to avoid byte[] overheadsnamespaceMicrosoft.Extensions.Caching.Distributed;publicinterfaceIBufferDistributedCache:IDistributedCache{boolTryGet(stringkey,IBufferWriter<byte>destination);ValueTask<bool>TryGetAsync(stringkey,IBufferWriter<byte>destination,CancellationTokentoken=default);voidSet(stringkey,ReadOnlySequence<byte>value,DistributedCacheEntryOptionsoptions);ValueTaskSetAsync(stringkey,ReadOnlySequence<byte>value,DistributedCacheEntryOptionsoptions,CancellationTokentoken=default);}// define abstract API for new HybridCache systemnamespaceMicrosoft.Extensions.Caching.Hybrid;publicabstractclassHybridCache{publicabstractValueTask<T>GetOrCreateAsync<TState,T>(stringkey,TStatestate,Func<TState,CancellationToken,ValueTask<T>>factory,HybridCacheEntryOptions?options=null,IEnumerable<string>?tags=null,CancellationTokencancellationToken=default);publicValueTask<T>GetOrCreateAsync<T>(stringkey,Func<CancellationToken,ValueTask<T>>factory,HybridCacheEntryOptions?options=null,IEnumerable<string>?tags=null,CancellationTokencancellationToken=default);publicabstractValueTaskSetAsync<T>(stringkey,Tvalue,HybridCacheEntryOptions?options=null,IEnumerable<string>?tags=null,CancellationTokencancellationToken=default);publicabstractValueTaskRemoveAsync(stringkey,CancellationTokencancellationToken=default);publicvirtualValueTaskRemoveAsync(IEnumerable<string>keys,CancellationTokencancellationToken=default);publicvirtualValueTaskRemoveByTagAsync(IEnumerable<string>tags,CancellationTokencancellationToken=default);publicabstractValueTaskRemoveByTagAsync(stringtag,CancellationTokencancellationToken=default);}publicsealedclassHybridCacheEntryOptions{publicTimeSpan?Expiration{get;init;}publicTimeSpan?LocalCacheExpiration{get;init;}publicHybridCacheEntryFlags?Flags{get;init;}}[Flags]publicenumHybridCacheEntryFlags{None=0,DisableLocalCacheRead=1<<0,DisableLocalCacheWrite=1<<1,DisableLocalCache=DisableLocalCacheRead|DisableLocalCacheWrite,DisableDistributedCacheRead=1<<2,DisableDistributedCacheWrite=1<<3,DisableDistributedCache=DisableDistributedCacheRead|DisableDistributedCacheWrite,DisableUnderlyingData=1<<4,DisableCompression=1<<5,}publicinterfaceIHybridCacheSerializer<T>{TDeserialize(ReadOnlySequence<byte>source);voidSerialize(Tvalue,IBufferWriter<byte>target);}publicinterfaceIHybridCacheSerializerFactory{boolTryCreateSerializer<T>([NotNullWhen(true)]outIHybridCacheSerializer<T>?serializer);}

              (edit: changed name from cancellationToken to token to mirror IDistributedCache, and added = default)
              (edit: added the sync paths)

              API Usage

              The usage of this API is optional; existing backends that implement IDistributedCachemay choose (or not) to additionally implement the new API. The new "hybrid cache" piece will type-test for the feature, and use it appropriately. Any backends that do not implement the API: continue to work, using the byte[] allocation.

              The design for "set" is simple: the caller owns the memory lifetime via ReadOnlySequence<byte> - the backend (as an API contract) is explicitly meant to copy the data out; storing the passed in value is undefined behaviour as that data may go out of scope.

              The design for "get" is for the caller to handle memory management (which would otherwise be duplicated and brittle in every backend); this is achieved by passing in an IBufferWriter<byte> to which the backend can push the data. This also means that the "hybrid cache" piece can handle quotas etc before data is fully read. The bool return-value is used to distinguish "found" vs "not found"; this is null vs not null on the old API, and is necessary because zero bytes is a valid payload length in some formats (I'm looking at you, protobuf).


              Note that hybrid cache proposal only uses async fetch; however, it is noted that if we only added async methods, this would mean that IBufferDistributedCache is "unbalanced" vs IDistributedCache (sync vs async), and omitting it would limit our options later if we decide to add sync paths on hybrid cache; accordingly, sync get+set paths are included in this proposal.

              Alternative Designs

              • Stream - allocatey, indirect, and multi-copylicious; significant complications for producer and consumer
              • IMemoryOwner<byte> or similar return - contiguous, caller gets no chance to intercept until all prepared
              • ReadOnlySpan<byte> input (to avoid storage) - contiguous, only applies to "sync"
              • default interface methods rather than new interface - would be a similar API change, but would involve an extra memcpy and lease for each fetch (default implementation would be "get array, write array to buffer-writer, which in turn needs to lease"); it is preferable to type test instead (once at setup), and use the most efficient strategy
              • naming... yeah, I'm open to offers

              Risks

              None; any existing backends not implementing the feature continue to work as current, hopefully adding support in time; there is no additional service registration for this auxiliary API

              Metadata

              Metadata

              Assignees

              No one assigned

                Labels

                api-approvedAPI was approved in API review, it can be implementedarea-Extensions-CachingblockingMarks issues that we want to fast track in order to unblock other important work

                Type

                No type

                Projects

                No projects

                  Milestone

                  Relationships

                  None yet

                  Development

                  No branches or pull requests

                  Issue actions

                  , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
                  Skip to content

                  [API Proposal]: Migrate HybridCache from aspnet to runtime  #100290

                  Description

                  @mgravell

                  Background and motivation

                  Context; this is part of Epic: IDistributedCache updates in .NET 9 and Hybrid Cache API proposal

                  Was originally just IDistributedCache, but this issue now updated to include all of the abstract HybridCache API (but not the aspnet implementation)

                  HybridCache is a new cache abstraction that sits on top of IDistributedCache (L2) and IMemoryCache (L1) to provide an integrated cache experience that includes serialization, stampede protection, and a range of other features. The API has been discussed extensively as part of aspnet, most significantly in the aforementioned dotnet/aspnetcore#54647.

                  We would now like to kick the aspnet bits over the fence into Microsoft.Extensions.Caching.Abstractions, so that:

                  • backend implementations such as Redis, SQL, etc do not need an aspnet framework reference
                  • applications other than aspnet can consume the APIs
                  • other non-aspnet implementations are possible (in particular, FusionCache have expressed interest)

                  The specific proposed API changes are laid out in #103103


                  The existing IDistributedCache API is based around byte[], which is wildly inefficient for anything that isn't an in-memory lookup of string to byte[] (i.e. handing back the same array each time, which is itself a bit dangerous because of array mutation).

                  To be 100% explicit:

                  • because the byte[] needs to be right-sized, it must be allocated per usage (especially if we want defensive copies)
                  • even if we knew the length, to use an array-segment efficiently we would also need to agree a recycling strategy between caller and callee
                  • byte[] demands contiguous memory, which can force LOH etc

                  The proposal is to add non-allocating APIs, similar to those used for Output Cache in .NET 8, to avoid these allocations; this assists every other backend - Redis, SQL, SQLite, Cosmos, etc.

                  As an example of the impact of this, see this table (note also the second table in the same comment), where a mocked up version of the API was used to test a FASTER-based cache backend (this is a useful backend because it has very low internal overheads).

                  | Method | KeyLength | PayloadLength | Mean | Error | StdDev | Gen0 | Gen1 | Allocated ||--------------- |---------- |-------------- |------------:|------------:|------------:|-------:|-------:|----------:|| Get | 128 | 10240 | 576.0 ns | 9.79 ns | 5.83 ns | 0.6123 | - | 10264 B || Set | 128 | 10240 | 882.0 ns | 23.99 ns | 22.44 ns | 0.6123 | - | 10264 B || GetAsync | 128 | 10240 | 657.6 ns | 16.96 ns | 14.16 ns | 0.6189 | - | 10360 B || SetAsync | 128 | 10240 | 1,094.7 ns | 55.15 ns | 51.58 ns | 0.6123 | - | 10264 B || | | | | | | | | || GetBuffer | 128 | 10240 | 366.1 ns | 6.22 ns | 5.20 ns | - | - | - || SetBuffer | 128 | 10240 | 495.4 ns | 7.11 ns | 2.54 ns | - | - | - || GetAsyncBuffer | 128 | 10240 | 387.9 ns | 7.60 ns | 1.97 ns | 0.0014 | - | 24 B || SetAsyncBuffer | 128 | 10240 | 649.9 ns | 12.70 ns | 11.88 ns | - | - | - |

                  API Proposal

                  // add extension API for existing IDistributedCache, to avoid byte[] overheadsnamespaceMicrosoft.Extensions.Caching.Distributed;publicinterfaceIBufferDistributedCache:IDistributedCache{boolTryGet(stringkey,IBufferWriter<byte>destination);ValueTask<bool>TryGetAsync(stringkey,IBufferWriter<byte>destination,CancellationTokentoken=default);voidSet(stringkey,ReadOnlySequence<byte>value,DistributedCacheEntryOptionsoptions);ValueTaskSetAsync(stringkey,ReadOnlySequence<byte>value,DistributedCacheEntryOptionsoptions,CancellationTokentoken=default);}// define abstract API for new HybridCache systemnamespaceMicrosoft.Extensions.Caching.Hybrid;publicabstractclassHybridCache{publicabstractValueTask<T>GetOrCreateAsync<TState,T>(stringkey,TStatestate,Func<TState,CancellationToken,ValueTask<T>>factory,HybridCacheEntryOptions?options=null,IEnumerable<string>?tags=null,CancellationTokencancellationToken=default);publicValueTask<T>GetOrCreateAsync<T>(stringkey,Func<CancellationToken,ValueTask<T>>factory,HybridCacheEntryOptions?options=null,IEnumerable<string>?tags=null,CancellationTokencancellationToken=default);publicabstractValueTaskSetAsync<T>(stringkey,Tvalue,HybridCacheEntryOptions?options=null,IEnumerable<string>?tags=null,CancellationTokencancellationToken=default);publicabstractValueTaskRemoveAsync(stringkey,CancellationTokencancellationToken=default);publicvirtualValueTaskRemoveAsync(IEnumerable<string>keys,CancellationTokencancellationToken=default);publicvirtualValueTaskRemoveByTagAsync(IEnumerable<string>tags,CancellationTokencancellationToken=default);publicabstractValueTaskRemoveByTagAsync(stringtag,CancellationTokencancellationToken=default);}publicsealedclassHybridCacheEntryOptions{publicTimeSpan?Expiration{get;init;}publicTimeSpan?LocalCacheExpiration{get;init;}publicHybridCacheEntryFlags?Flags{get;init;}}[Flags]publicenumHybridCacheEntryFlags{None=0,DisableLocalCacheRead=1<<0,DisableLocalCacheWrite=1<<1,DisableLocalCache=DisableLocalCacheRead|DisableLocalCacheWrite,DisableDistributedCacheRead=1<<2,DisableDistributedCacheWrite=1<<3,DisableDistributedCache=DisableDistributedCacheRead|DisableDistributedCacheWrite,DisableUnderlyingData=1<<4,DisableCompression=1<<5,}publicinterfaceIHybridCacheSerializer<T>{TDeserialize(ReadOnlySequence<byte>source);voidSerialize(Tvalue,IBufferWriter<byte>target);}publicinterfaceIHybridCacheSerializerFactory{boolTryCreateSerializer<T>([NotNullWhen(true)]outIHybridCacheSerializer<T>?serializer);}

                  (edit: changed name from cancellationToken to token to mirror IDistributedCache, and added = default)
                  (edit: added the sync paths)

                  API Usage

                  The usage of this API is optional; existing backends that implement IDistributedCachemay choose (or not) to additionally implement the new API. The new "hybrid cache" piece will type-test for the feature, and use it appropriately. Any backends that do not implement the API: continue to work, using the byte[] allocation.

                  The design for "set" is simple: the caller owns the memory lifetime via ReadOnlySequence<byte> - the backend (as an API contract) is explicitly meant to copy the data out; storing the passed in value is undefined behaviour as that data may go out of scope.

                  The design for "get" is for the caller to handle memory management (which would otherwise be duplicated and brittle in every backend); this is achieved by passing in an IBufferWriter<byte> to which the backend can push the data. This also means that the "hybrid cache" piece can handle quotas etc before data is fully read. The bool return-value is used to distinguish "found" vs "not found"; this is null vs not null on the old API, and is necessary because zero bytes is a valid payload length in some formats (I'm looking at you, protobuf).


                  Note that hybrid cache proposal only uses async fetch; however, it is noted that if we only added async methods, this would mean that IBufferDistributedCache is "unbalanced" vs IDistributedCache (sync vs async), and omitting it would limit our options later if we decide to add sync paths on hybrid cache; accordingly, sync get+set paths are included in this proposal.

                  Alternative Designs

                  • Stream - allocatey, indirect, and multi-copylicious; significant complications for producer and consumer
                  • IMemoryOwner<byte> or similar return - contiguous, caller gets no chance to intercept until all prepared
                  • ReadOnlySpan<byte> input (to avoid storage) - contiguous, only applies to "sync"
                  • default interface methods rather than new interface - would be a similar API change, but would involve an extra memcpy and lease for each fetch (default implementation would be "get array, write array to buffer-writer, which in turn needs to lease"); it is preferable to type test instead (once at setup), and use the most efficient strategy
                  • naming... yeah, I'm open to offers

                  Risks

                  None; any existing backends not implementing the feature continue to work as current, hopefully adding support in time; there is no additional service registration for this auxiliary API

                  Metadata

                  Metadata

                  Assignees

                  No one assigned

                    Labels

                    api-approvedAPI was approved in API review, it can be implementedarea-Extensions-CachingblockingMarks issues that we want to fast track in order to unblock other important work

                    Type

                    No type

                    Projects

                    No projects

                      Milestone

                      Relationships

                      None yet

                      Development

                      No branches or pull requests

                      Issue actions

                      , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
                      Skip to content

                      [API Proposal]: Migrate HybridCache from aspnet to runtime  #100290

                      Description

                      @mgravell

                      Background and motivation

                      Context; this is part of Epic: IDistributedCache updates in .NET 9 and Hybrid Cache API proposal

                      Was originally just IDistributedCache, but this issue now updated to include all of the abstract HybridCache API (but not the aspnet implementation)

                      HybridCache is a new cache abstraction that sits on top of IDistributedCache (L2) and IMemoryCache (L1) to provide an integrated cache experience that includes serialization, stampede protection, and a range of other features. The API has been discussed extensively as part of aspnet, most significantly in the aforementioned dotnet/aspnetcore#54647.

                      We would now like to kick the aspnet bits over the fence into Microsoft.Extensions.Caching.Abstractions, so that:

                      • backend implementations such as Redis, SQL, etc do not need an aspnet framework reference
                      • applications other than aspnet can consume the APIs
                      • other non-aspnet implementations are possible (in particular, FusionCache have expressed interest)

                      The specific proposed API changes are laid out in #103103


                      The existing IDistributedCache API is based around byte[], which is wildly inefficient for anything that isn't an in-memory lookup of string to byte[] (i.e. handing back the same array each time, which is itself a bit dangerous because of array mutation).

                      To be 100% explicit:

                      • because the byte[] needs to be right-sized, it must be allocated per usage (especially if we want defensive copies)
                      • even if we knew the length, to use an array-segment efficiently we would also need to agree a recycling strategy between caller and callee
                      • byte[] demands contiguous memory, which can force LOH etc

                      The proposal is to add non-allocating APIs, similar to those used for Output Cache in .NET 8, to avoid these allocations; this assists every other backend - Redis, SQL, SQLite, Cosmos, etc.

                      As an example of the impact of this, see this table (note also the second table in the same comment), where a mocked up version of the API was used to test a FASTER-based cache backend (this is a useful backend because it has very low internal overheads).

                      | Method | KeyLength | PayloadLength | Mean | Error | StdDev | Gen0 | Gen1 | Allocated ||--------------- |---------- |-------------- |------------:|------------:|------------:|-------:|-------:|----------:|| Get | 128 | 10240 | 576.0 ns | 9.79 ns | 5.83 ns | 0.6123 | - | 10264 B || Set | 128 | 10240 | 882.0 ns | 23.99 ns | 22.44 ns | 0.6123 | - | 10264 B || GetAsync | 128 | 10240 | 657.6 ns | 16.96 ns | 14.16 ns | 0.6189 | - | 10360 B || SetAsync | 128 | 10240 | 1,094.7 ns | 55.15 ns | 51.58 ns | 0.6123 | - | 10264 B || | | | | | | | | || GetBuffer | 128 | 10240 | 366.1 ns | 6.22 ns | 5.20 ns | - | - | - || SetBuffer | 128 | 10240 | 495.4 ns | 7.11 ns | 2.54 ns | - | - | - || GetAsyncBuffer | 128 | 10240 | 387.9 ns | 7.60 ns | 1.97 ns | 0.0014 | - | 24 B || SetAsyncBuffer | 128 | 10240 | 649.9 ns | 12.70 ns | 11.88 ns | - | - | - |

                      API Proposal

                      // add extension API for existing IDistributedCache, to avoid byte[] overheadsnamespaceMicrosoft.Extensions.Caching.Distributed;publicinterfaceIBufferDistributedCache:IDistributedCache{boolTryGet(stringkey,IBufferWriter<byte>destination);ValueTask<bool>TryGetAsync(stringkey,IBufferWriter<byte>destination,CancellationTokentoken=default);voidSet(stringkey,ReadOnlySequence<byte>value,DistributedCacheEntryOptionsoptions);ValueTaskSetAsync(stringkey,ReadOnlySequence<byte>value,DistributedCacheEntryOptionsoptions,CancellationTokentoken=default);}// define abstract API for new HybridCache systemnamespaceMicrosoft.Extensions.Caching.Hybrid;publicabstractclassHybridCache{publicabstractValueTask<T>GetOrCreateAsync<TState,T>(stringkey,TStatestate,Func<TState,CancellationToken,ValueTask<T>>factory,HybridCacheEntryOptions?options=null,IEnumerable<string>?tags=null,CancellationTokencancellationToken=default);publicValueTask<T>GetOrCreateAsync<T>(stringkey,Func<CancellationToken,ValueTask<T>>factory,HybridCacheEntryOptions?options=null,IEnumerable<string>?tags=null,CancellationTokencancellationToken=default);publicabstractValueTaskSetAsync<T>(stringkey,Tvalue,HybridCacheEntryOptions?options=null,IEnumerable<string>?tags=null,CancellationTokencancellationToken=default);publicabstractValueTaskRemoveAsync(stringkey,CancellationTokencancellationToken=default);publicvirtualValueTaskRemoveAsync(IEnumerable<string>keys,CancellationTokencancellationToken=default);publicvirtualValueTaskRemoveByTagAsync(IEnumerable<string>tags,CancellationTokencancellationToken=default);publicabstractValueTaskRemoveByTagAsync(stringtag,CancellationTokencancellationToken=default);}publicsealedclassHybridCacheEntryOptions{publicTimeSpan?Expiration{get;init;}publicTimeSpan?LocalCacheExpiration{get;init;}publicHybridCacheEntryFlags?Flags{get;init;}}[Flags]publicenumHybridCacheEntryFlags{None=0,DisableLocalCacheRead=1<<0,DisableLocalCacheWrite=1<<1,DisableLocalCache=DisableLocalCacheRead|DisableLocalCacheWrite,DisableDistributedCacheRead=1<<2,DisableDistributedCacheWrite=1<<3,DisableDistributedCache=DisableDistributedCacheRead|DisableDistributedCacheWrite,DisableUnderlyingData=1<<4,DisableCompression=1<<5,}publicinterfaceIHybridCacheSerializer<T>{TDeserialize(ReadOnlySequence<byte>source);voidSerialize(Tvalue,IBufferWriter<byte>target);}publicinterfaceIHybridCacheSerializerFactory{boolTryCreateSerializer<T>([NotNullWhen(true)]outIHybridCacheSerializer<T>?serializer);}

                      (edit: changed name from cancellationToken to token to mirror IDistributedCache, and added = default)
                      (edit: added the sync paths)

                      API Usage

                      The usage of this API is optional; existing backends that implement IDistributedCachemay choose (or not) to additionally implement the new API. The new "hybrid cache" piece will type-test for the feature, and use it appropriately. Any backends that do not implement the API: continue to work, using the byte[] allocation.

                      The design for "set" is simple: the caller owns the memory lifetime via ReadOnlySequence<byte> - the backend (as an API contract) is explicitly meant to copy the data out; storing the passed in value is undefined behaviour as that data may go out of scope.

                      The design for "get" is for the caller to handle memory management (which would otherwise be duplicated and brittle in every backend); this is achieved by passing in an IBufferWriter<byte> to which the backend can push the data. This also means that the "hybrid cache" piece can handle quotas etc before data is fully read. The bool return-value is used to distinguish "found" vs "not found"; this is null vs not null on the old API, and is necessary because zero bytes is a valid payload length in some formats (I'm looking at you, protobuf).


                      Note that hybrid cache proposal only uses async fetch; however, it is noted that if we only added async methods, this would mean that IBufferDistributedCache is "unbalanced" vs IDistributedCache (sync vs async), and omitting it would limit our options later if we decide to add sync paths on hybrid cache; accordingly, sync get+set paths are included in this proposal.

                      Alternative Designs

                      • Stream - allocatey, indirect, and multi-copylicious; significant complications for producer and consumer
                      • IMemoryOwner<byte> or similar return - contiguous, caller gets no chance to intercept until all prepared
                      • ReadOnlySpan<byte> input (to avoid storage) - contiguous, only applies to "sync"
                      • default interface methods rather than new interface - would be a similar API change, but would involve an extra memcpy and lease for each fetch (default implementation would be "get array, write array to buffer-writer, which in turn needs to lease"); it is preferable to type test instead (once at setup), and use the most efficient strategy
                      • naming... yeah, I'm open to offers

                      Risks

                      None; any existing backends not implementing the feature continue to work as current, hopefully adding support in time; there is no additional service registration for this auxiliary API

                      Metadata

                      Metadata

                      Assignees

                      No one assigned

                        Labels

                        api-approvedAPI was approved in API review, it can be implementedarea-Extensions-CachingblockingMarks issues that we want to fast track in order to unblock other important work

                        Type

                        No type

                        Projects

                        No projects

                          Milestone

                          Relationships

                          None yet

                          Development

                          No branches or pull requests

                          Issue actions

                          , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
                          Skip to content

                          [API Proposal]: Migrate HybridCache from aspnet to runtime  #100290

                          Description

                          @mgravell

                          Background and motivation

                          Context; this is part of Epic: IDistributedCache updates in .NET 9 and Hybrid Cache API proposal

                          Was originally just IDistributedCache, but this issue now updated to include all of the abstract HybridCache API (but not the aspnet implementation)

                          HybridCache is a new cache abstraction that sits on top of IDistributedCache (L2) and IMemoryCache (L1) to provide an integrated cache experience that includes serialization, stampede protection, and a range of other features. The API has been discussed extensively as part of aspnet, most significantly in the aforementioned dotnet/aspnetcore#54647.

                          We would now like to kick the aspnet bits over the fence into Microsoft.Extensions.Caching.Abstractions, so that:

                          • backend implementations such as Redis, SQL, etc do not need an aspnet framework reference
                          • applications other than aspnet can consume the APIs
                          • other non-aspnet implementations are possible (in particular, FusionCache have expressed interest)

                          The specific proposed API changes are laid out in #103103


                          The existing IDistributedCache API is based around byte[], which is wildly inefficient for anything that isn't an in-memory lookup of string to byte[] (i.e. handing back the same array each time, which is itself a bit dangerous because of array mutation).

                          To be 100% explicit:

                          • because the byte[] needs to be right-sized, it must be allocated per usage (especially if we want defensive copies)
                          • even if we knew the length, to use an array-segment efficiently we would also need to agree a recycling strategy between caller and callee
                          • byte[] demands contiguous memory, which can force LOH etc

                          The proposal is to add non-allocating APIs, similar to those used for Output Cache in .NET 8, to avoid these allocations; this assists every other backend - Redis, SQL, SQLite, Cosmos, etc.

                          As an example of the impact of this, see this table (note also the second table in the same comment), where a mocked up version of the API was used to test a FASTER-based cache backend (this is a useful backend because it has very low internal overheads).

                          | Method | KeyLength | PayloadLength | Mean | Error | StdDev | Gen0 | Gen1 | Allocated ||--------------- |---------- |-------------- |------------:|------------:|------------:|-------:|-------:|----------:|| Get | 128 | 10240 | 576.0 ns | 9.79 ns | 5.83 ns | 0.6123 | - | 10264 B || Set | 128 | 10240 | 882.0 ns | 23.99 ns | 22.44 ns | 0.6123 | - | 10264 B || GetAsync | 128 | 10240 | 657.6 ns | 16.96 ns | 14.16 ns | 0.6189 | - | 10360 B || SetAsync | 128 | 10240 | 1,094.7 ns | 55.15 ns | 51.58 ns | 0.6123 | - | 10264 B || | | | | | | | | || GetBuffer | 128 | 10240 | 366.1 ns | 6.22 ns | 5.20 ns | - | - | - || SetBuffer | 128 | 10240 | 495.4 ns | 7.11 ns | 2.54 ns | - | - | - || GetAsyncBuffer | 128 | 10240 | 387.9 ns | 7.60 ns | 1.97 ns | 0.0014 | - | 24 B || SetAsyncBuffer | 128 | 10240 | 649.9 ns | 12.70 ns | 11.88 ns | - | - | - |

                          API Proposal

                          // add extension API for existing IDistributedCache, to avoid byte[] overheadsnamespaceMicrosoft.Extensions.Caching.Distributed;publicinterfaceIBufferDistributedCache:IDistributedCache{boolTryGet(stringkey,IBufferWriter<byte>destination);ValueTask<bool>TryGetAsync(stringkey,IBufferWriter<byte>destination,CancellationTokentoken=default);voidSet(stringkey,ReadOnlySequence<byte>value,DistributedCacheEntryOptionsoptions);ValueTaskSetAsync(stringkey,ReadOnlySequence<byte>value,DistributedCacheEntryOptionsoptions,CancellationTokentoken=default);}// define abstract API for new HybridCache systemnamespaceMicrosoft.Extensions.Caching.Hybrid;publicabstractclassHybridCache{publicabstractValueTask<T>GetOrCreateAsync<TState,T>(stringkey,TStatestate,Func<TState,CancellationToken,ValueTask<T>>factory,HybridCacheEntryOptions?options=null,IEnumerable<string>?tags=null,CancellationTokencancellationToken=default);publicValueTask<T>GetOrCreateAsync<T>(stringkey,Func<CancellationToken,ValueTask<T>>factory,HybridCacheEntryOptions?options=null,IEnumerable<string>?tags=null,CancellationTokencancellationToken=default);publicabstractValueTaskSetAsync<T>(stringkey,Tvalue,HybridCacheEntryOptions?options=null,IEnumerable<string>?tags=null,CancellationTokencancellationToken=default);publicabstractValueTaskRemoveAsync(stringkey,CancellationTokencancellationToken=default);publicvirtualValueTaskRemoveAsync(IEnumerable<string>keys,CancellationTokencancellationToken=default);publicvirtualValueTaskRemoveByTagAsync(IEnumerable<string>tags,CancellationTokencancellationToken=default);publicabstractValueTaskRemoveByTagAsync(stringtag,CancellationTokencancellationToken=default);}publicsealedclassHybridCacheEntryOptions{publicTimeSpan?Expiration{get;init;}publicTimeSpan?LocalCacheExpiration{get;init;}publicHybridCacheEntryFlags?Flags{get;init;}}[Flags]publicenumHybridCacheEntryFlags{None=0,DisableLocalCacheRead=1<<0,DisableLocalCacheWrite=1<<1,DisableLocalCache=DisableLocalCacheRead|DisableLocalCacheWrite,DisableDistributedCacheRead=1<<2,DisableDistributedCacheWrite=1<<3,DisableDistributedCache=DisableDistributedCacheRead|DisableDistributedCacheWrite,DisableUnderlyingData=1<<4,DisableCompression=1<<5,}publicinterfaceIHybridCacheSerializer<T>{TDeserialize(ReadOnlySequence<byte>source);voidSerialize(Tvalue,IBufferWriter<byte>target);}publicinterfaceIHybridCacheSerializerFactory{boolTryCreateSerializer<T>([NotNullWhen(true)]outIHybridCacheSerializer<T>?serializer);}

                          (edit: changed name from cancellationToken to token to mirror IDistributedCache, and added = default)
                          (edit: added the sync paths)

                          API Usage

                          The usage of this API is optional; existing backends that implement IDistributedCachemay choose (or not) to additionally implement the new API. The new "hybrid cache" piece will type-test for the feature, and use it appropriately. Any backends that do not implement the API: continue to work, using the byte[] allocation.

                          The design for "set" is simple: the caller owns the memory lifetime via ReadOnlySequence<byte> - the backend (as an API contract) is explicitly meant to copy the data out; storing the passed in value is undefined behaviour as that data may go out of scope.

                          The design for "get" is for the caller to handle memory management (which would otherwise be duplicated and brittle in every backend); this is achieved by passing in an IBufferWriter<byte> to which the backend can push the data. This also means that the "hybrid cache" piece can handle quotas etc before data is fully read. The bool return-value is used to distinguish "found" vs "not found"; this is null vs not null on the old API, and is necessary because zero bytes is a valid payload length in some formats (I'm looking at you, protobuf).


                          Note that hybrid cache proposal only uses async fetch; however, it is noted that if we only added async methods, this would mean that IBufferDistributedCache is "unbalanced" vs IDistributedCache (sync vs async), and omitting it would limit our options later if we decide to add sync paths on hybrid cache; accordingly, sync get+set paths are included in this proposal.

                          Alternative Designs

                          • Stream - allocatey, indirect, and multi-copylicious; significant complications for producer and consumer
                          • IMemoryOwner<byte> or similar return - contiguous, caller gets no chance to intercept until all prepared
                          • ReadOnlySpan<byte> input (to avoid storage) - contiguous, only applies to "sync"
                          • default interface methods rather than new interface - would be a similar API change, but would involve an extra memcpy and lease for each fetch (default implementation would be "get array, write array to buffer-writer, which in turn needs to lease"); it is preferable to type test instead (once at setup), and use the most efficient strategy
                          • naming... yeah, I'm open to offers

                          Risks

                          None; any existing backends not implementing the feature continue to work as current, hopefully adding support in time; there is no additional service registration for this auxiliary API

                          Metadata

                          Metadata

                          Assignees

                          No one assigned

                            Labels

                            api-approvedAPI was approved in API review, it can be implementedarea-Extensions-CachingblockingMarks issues that we want to fast track in order to unblock other important work

                            Type

                            No type

                            Projects

                            No projects

                              Milestone

                              Relationships

                              None yet

                              Development

                              No branches or pull requests

                              Issue actions

                              , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
                              Skip to content

                              [API Proposal]: Migrate HybridCache from aspnet to runtime  #100290

                              Description

                              @mgravell

                              Background and motivation

                              Context; this is part of Epic: IDistributedCache updates in .NET 9 and Hybrid Cache API proposal

                              Was originally just IDistributedCache, but this issue now updated to include all of the abstract HybridCache API (but not the aspnet implementation)

                              HybridCache is a new cache abstraction that sits on top of IDistributedCache (L2) and IMemoryCache (L1) to provide an integrated cache experience that includes serialization, stampede protection, and a range of other features. The API has been discussed extensively as part of aspnet, most significantly in the aforementioned dotnet/aspnetcore#54647.

                              We would now like to kick the aspnet bits over the fence into Microsoft.Extensions.Caching.Abstractions, so that:

                              • backend implementations such as Redis, SQL, etc do not need an aspnet framework reference
                              • applications other than aspnet can consume the APIs
                              • other non-aspnet implementations are possible (in particular, FusionCache have expressed interest)

                              The specific proposed API changes are laid out in #103103


                              The existing IDistributedCache API is based around byte[], which is wildly inefficient for anything that isn't an in-memory lookup of string to byte[] (i.e. handing back the same array each time, which is itself a bit dangerous because of array mutation).

                              To be 100% explicit:

                              • because the byte[] needs to be right-sized, it must be allocated per usage (especially if we want defensive copies)
                              • even if we knew the length, to use an array-segment efficiently we would also need to agree a recycling strategy between caller and callee
                              • byte[] demands contiguous memory, which can force LOH etc

                              The proposal is to add non-allocating APIs, similar to those used for Output Cache in .NET 8, to avoid these allocations; this assists every other backend - Redis, SQL, SQLite, Cosmos, etc.

                              As an example of the impact of this, see this table (note also the second table in the same comment), where a mocked up version of the API was used to test a FASTER-based cache backend (this is a useful backend because it has very low internal overheads).

                              | Method | KeyLength | PayloadLength | Mean | Error | StdDev | Gen0 | Gen1 | Allocated ||--------------- |---------- |-------------- |------------:|------------:|------------:|-------:|-------:|----------:|| Get | 128 | 10240 | 576.0 ns | 9.79 ns | 5.83 ns | 0.6123 | - | 10264 B || Set | 128 | 10240 | 882.0 ns | 23.99 ns | 22.44 ns | 0.6123 | - | 10264 B || GetAsync | 128 | 10240 | 657.6 ns | 16.96 ns | 14.16 ns | 0.6189 | - | 10360 B || SetAsync | 128 | 10240 | 1,094.7 ns | 55.15 ns | 51.58 ns | 0.6123 | - | 10264 B || | | | | | | | | || GetBuffer | 128 | 10240 | 366.1 ns | 6.22 ns | 5.20 ns | - | - | - || SetBuffer | 128 | 10240 | 495.4 ns | 7.11 ns | 2.54 ns | - | - | - || GetAsyncBuffer | 128 | 10240 | 387.9 ns | 7.60 ns | 1.97 ns | 0.0014 | - | 24 B || SetAsyncBuffer | 128 | 10240 | 649.9 ns | 12.70 ns | 11.88 ns | - | - | - |

                              API Proposal

                              // add extension API for existing IDistributedCache, to avoid byte[] overheadsnamespaceMicrosoft.Extensions.Caching.Distributed;publicinterfaceIBufferDistributedCache:IDistributedCache{boolTryGet(stringkey,IBufferWriter<byte>destination);ValueTask<bool>TryGetAsync(stringkey,IBufferWriter<byte>destination,CancellationTokentoken=default);voidSet(stringkey,ReadOnlySequence<byte>value,DistributedCacheEntryOptionsoptions);ValueTaskSetAsync(stringkey,ReadOnlySequence<byte>value,DistributedCacheEntryOptionsoptions,CancellationTokentoken=default);}// define abstract API for new HybridCache systemnamespaceMicrosoft.Extensions.Caching.Hybrid;publicabstractclassHybridCache{publicabstractValueTask<T>GetOrCreateAsync<TState,T>(stringkey,TStatestate,Func<TState,CancellationToken,ValueTask<T>>factory,HybridCacheEntryOptions?options=null,IEnumerable<string>?tags=null,CancellationTokencancellationToken=default);publicValueTask<T>GetOrCreateAsync<T>(stringkey,Func<CancellationToken,ValueTask<T>>factory,HybridCacheEntryOptions?options=null,IEnumerable<string>?tags=null,CancellationTokencancellationToken=default);publicabstractValueTaskSetAsync<T>(stringkey,Tvalue,HybridCacheEntryOptions?options=null,IEnumerable<string>?tags=null,CancellationTokencancellationToken=default);publicabstractValueTaskRemoveAsync(stringkey,CancellationTokencancellationToken=default);publicvirtualValueTaskRemoveAsync(IEnumerable<string>keys,CancellationTokencancellationToken=default);publicvirtualValueTaskRemoveByTagAsync(IEnumerable<string>tags,CancellationTokencancellationToken=default);publicabstractValueTaskRemoveByTagAsync(stringtag,CancellationTokencancellationToken=default);}publicsealedclassHybridCacheEntryOptions{publicTimeSpan?Expiration{get;init;}publicTimeSpan?LocalCacheExpiration{get;init;}publicHybridCacheEntryFlags?Flags{get;init;}}[Flags]publicenumHybridCacheEntryFlags{None=0,DisableLocalCacheRead=1<<0,DisableLocalCacheWrite=1<<1,DisableLocalCache=DisableLocalCacheRead|DisableLocalCacheWrite,DisableDistributedCacheRead=1<<2,DisableDistributedCacheWrite=1<<3,DisableDistributedCache=DisableDistributedCacheRead|DisableDistributedCacheWrite,DisableUnderlyingData=1<<4,DisableCompression=1<<5,}publicinterfaceIHybridCacheSerializer<T>{TDeserialize(ReadOnlySequence<byte>source);voidSerialize(Tvalue,IBufferWriter<byte>target);}publicinterfaceIHybridCacheSerializerFactory{boolTryCreateSerializer<T>([NotNullWhen(true)]outIHybridCacheSerializer<T>?serializer);}

                              (edit: changed name from cancellationToken to token to mirror IDistributedCache, and added = default)
                              (edit: added the sync paths)

                              API Usage

                              The usage of this API is optional; existing backends that implement IDistributedCachemay choose (or not) to additionally implement the new API. The new "hybrid cache" piece will type-test for the feature, and use it appropriately. Any backends that do not implement the API: continue to work, using the byte[] allocation.

                              The design for "set" is simple: the caller owns the memory lifetime via ReadOnlySequence<byte> - the backend (as an API contract) is explicitly meant to copy the data out; storing the passed in value is undefined behaviour as that data may go out of scope.

                              The design for "get" is for the caller to handle memory management (which would otherwise be duplicated and brittle in every backend); this is achieved by passing in an IBufferWriter<byte> to which the backend can push the data. This also means that the "hybrid cache" piece can handle quotas etc before data is fully read. The bool return-value is used to distinguish "found" vs "not found"; this is null vs not null on the old API, and is necessary because zero bytes is a valid payload length in some formats (I'm looking at you, protobuf).


                              Note that hybrid cache proposal only uses async fetch; however, it is noted that if we only added async methods, this would mean that IBufferDistributedCache is "unbalanced" vs IDistributedCache (sync vs async), and omitting it would limit our options later if we decide to add sync paths on hybrid cache; accordingly, sync get+set paths are included in this proposal.

                              Alternative Designs

                              • Stream - allocatey, indirect, and multi-copylicious; significant complications for producer and consumer
                              • IMemoryOwner<byte> or similar return - contiguous, caller gets no chance to intercept until all prepared
                              • ReadOnlySpan<byte> input (to avoid storage) - contiguous, only applies to "sync"
                              • default interface methods rather than new interface - would be a similar API change, but would involve an extra memcpy and lease for each fetch (default implementation would be "get array, write array to buffer-writer, which in turn needs to lease"); it is preferable to type test instead (once at setup), and use the most efficient strategy
                              • naming... yeah, I'm open to offers

                              Risks

                              None; any existing backends not implementing the feature continue to work as current, hopefully adding support in time; there is no additional service registration for this auxiliary API

                              Metadata

                              Metadata

                              Assignees

                              No one assigned

                                Labels

                                api-approvedAPI was approved in API review, it can be implementedarea-Extensions-CachingblockingMarks issues that we want to fast track in order to unblock other important work

                                Type

                                No type

                                Projects

                                No projects

                                  Milestone

                                  Relationships

                                  None yet

                                  Development

                                  No branches or pull requests

                                  Issue actions