From 3d9a60b233d58858a498fea7fccc8fb77361b482 Mon Sep 17 00:00:00 2001 From: Tom Dykstra Date: Wed, 21 May 2025 11:39:05 -0700 Subject: [PATCH 01/10] AOT and Acrolynx --- aspnetcore/performance/caching/hybrid.md | 42 +++++++++++++++--------- 1 file changed, 27 insertions(+), 15 deletions(-) diff --git a/aspnetcore/performance/caching/hybrid.md b/aspnetcore/performance/caching/hybrid.md index fac0b825cec3..43cecaeefa57 100644 --- a/aspnetcore/performance/caching/hybrid.md +++ b/aspnetcore/performance/caching/hybrid.md @@ -57,7 +57,7 @@ Both types of uniqueness are usually ensured by using string concatenation to ma cache.GetOrCreateAsync($"/orders/{region}/{orderId}", ...); ``` -or +Or ```csharp cache.GetOrCreateAsync($"user_prefs_{userId}", ...); @@ -65,18 +65,18 @@ cache.GetOrCreateAsync($"user_prefs_{userId}", ...); It's the caller's responsibility to ensure that a key scheme is valid and can't cause data to become confused. - We recommend that you not use external user input in the cache key. For example, don't use raw `string` values from a UI as part of a cache key. Such keys could allow malicious access attempts, or could be used in a denial-of-service attack by saturating your cache with data having meaningless keys generated from random strings. In the preceding valid examples, the *order* data and *user preference* data are clearly distinct: +Avoid using external user input directly in cache keys. For example, don't use raw strings from user interfaces as cache keys. Doing so can expose your app to security risks, such as unauthorized access or denial-of-service attacks caused by flooding the cache with random or meaningless keys. In the valid examples above, the *order* and *user preference* data are clearly separated and use trusted identifiers: * `orderid` and `userId` are internally generated identifiers. * `region` might be an enum or string from a predefined list of known regions. -There is no significance placed on tokens such as `/` or `_`. The entire key value is treated as an opaque identifying string. In this case, you could omit the `/` and `_` with no +No significance is placed on tokens such as `/` or `_`. The entire key value is treated as an opaque identifying string. In this case, you could omit the `/` and `_` with no change to the way the cache functions, but a delimiter is usually used to avoid ambiguity - for example `$"order{customerId}{orderId}"` could cause confusion between: * `customerId` 42 with `orderId` 123 * `customerId` 421 with `orderId` 23 -(both of which would generate the cache key `order42123`) +Both of the preceding examples would generate the cache key `order42123`. This guidance applies equally to any `string`-based cache API, such as `HybridCache`, `IDistributedCache`, and `IMemoryCache`. @@ -84,13 +84,13 @@ Notice that the inline interpolated string syntax (`$"..."` in the preceding exa ### Additional key considerations -* Keys can be restricted to valid maximum lengths. For example, the default `HybridCache` implementation (via `AddHybridCache(...)`) restricts keys to 1024 characters by default. That number is configurable via `HybridCacheOptions.MaximumKeyLength`, with longer keys bypassing the cache mechanisms to prevent saturation. +* Keys can be restricted to valid maximum lengths. For example, the default `HybridCache` implementation (via `AddHybridCache(...)`) restricts keys to 1,024 characters by default. That number is configurable via `HybridCacheOptions.MaximumKeyLength`, with longer keys bypassing the cache mechanisms to prevent saturation. * Keys must be valid Unicode sequences. If invalid Unicode sequences are passed, the behavior is undefined. -* When using an out-of-process secondary cache such as `IDistributedCache`, the backend implementation may impose additional restrictions. As a hypothetical example, a particular backend might use case-insensitive key logic. The default `HybridCache` (via `AddHybridCache(...)`) detects this scenario to prevent confusion attacks or alias attacks (using bitwise string equality). However, this scenario might still result in conflicting keys becoming overwritten or evicted sooner than expected. +* When using an out-of-process secondary cache such as `IDistributedCache`, the backend implementation may impose additional restrictions. As a hypothetical example, a particular backend might use case-insensitive key logic. The default `HybridCache` (via `AddHybridCache(...)`) detects this scenario to prevent confusion attacks or alias attacks (using bitwise string equality). However, this scenario might still result in conflicting keys becoming overwritten or evicted sooner than expected. ### The alternative `GetOrCreateAsync` overload -The alternative overload might reduce some overhead from [captured variables](/dotnet/csharp/language-reference/operators/lambda-expressions#capture-of-outer-variables-and-variable-scope-in-lambda-expressions) and per-instance callbacks, but at the expense of more complex code. For most scenarios the performance increase doesn't outweigh the code complexity. Here's an example that uses the alternative overload: +The alternative overload might reduce some overhead from [captured variables](/dotnet/csharp/language-reference/operators/lambda-expressions#capture-of-outer-variables-and-variable-scope-in-lambda-expressions) and per-instance callbacks, but at the expense of more complex code. For most scenarios, the performance increase doesn't outweigh the code complexity. Here's an example that uses the alternative overload: :::code language="csharp" source="~/performance/caching/hybrid/samples/9.x/HCMinimal/Program.cs" id="snippet_getorcreatestate" highlight="5-14"::: @@ -117,11 +117,11 @@ Set tags when calling `GetOrCreateAsync`, as shown in the following example: Remove all entries for a specified tag by calling with the tag value. An overload lets you specify a collection of tag values. -Neither `IMemoryCache` nor `IDistributedCache` has direct support for the concept of tags, so tag-based invalidation is a *logical* operation only. It does not actively remove values from either local or distributed cache. Instead, it ensures that when receiving data with such tags, the data will be treated as a cache-miss from both the local and remote cache. The values will expire from `IMemoryCache` and `IDistributedCache` in the usual way based on the configured lifetime. +Neither `IMemoryCache` nor `IDistributedCache` has direct support for the concept of tags, so tag-based invalidation is a *logical* operation only. It doesn't actively remove values from either local or distributed cache. Instead, it ensures that when receiving data with such tags, the data is treated as a cache-miss from both the local and remote cache. The values expire from `IMemoryCache` and `IDistributedCache` in the usual way based on the configured lifetime. ## Removing all cache entries -The asterisk tag (`*`) is reserved as a wildcard and is disallowed against individual values. Calling `RemoveByTagAsync("*")` has the effect of invalidating *all* `HybridCache` data, even data that does not have any tags. As with individual tags, this is a *logical* operation, and individual values continue to exist until they expire naturally. Glob-style matches are not supported. For example, you can't use `RemoveByTagAsync("foo*")` to remove everything starting with `foo`. +The asterisk tag (`*`) is reserved as a wildcard and is disallowed against individual values. Calling `RemoveByTagAsync("*")` has the effect of invalidating *all* `HybridCache` data, even data that doesn't have any tags. As with individual tags, this is a *logical* operation, and individual values continue to exist until they expire naturally. Glob-style matches aren't supported. For example, you can't use `RemoveByTagAsync("foo*")` to remove everything starting with `foo`. ### Additional tag considerations @@ -191,7 +191,7 @@ For more information, see the [HybridCache serialization sample app](https://git By default `HybridCache` uses for its primary cache storage. Cache entries are stored in-process, so each server has a separate cache that is lost whenever the server process is restarted. For secondary out-of-process storage, such as Redis or SQL Server, `HybridCache` uses [the configured `IDistributedCache` implementation](xref:performance/caching/distributed), if any. But even without an `IDistributedCache`implementation, the `HybridCache` service still provides in-process caching and [stampede protection](https://en.wikipedia.org/wiki/Cache_stampede). > [!NOTE] -> When invalidating cache entries by key or by tags, they are invalidated in the current server and in the secondary out-of-process storage. However, the in-memory cache in other servers isn't affected. +> When invalidating cache entries by key or by tags, they're invalidated in the current server and in the secondary out-of-process storage. However, the in-memory cache in other servers isn't affected. ## Optimize performance @@ -205,7 +205,7 @@ In typical existing code that uses `IDistributedCache`, every retrieval of an ob Because much `HybridCache` usage will be adapted from existing `IDistributedCache` code, `HybridCache` preserves this behavior by default to avoid introducing concurrency bugs. However, objects are inherently thread-safe if: -* They are immutable types. +* They're immutable types. * The code doesn't modify them. In such cases, inform `HybridCache` that it's safe to reuse instances by: @@ -230,13 +230,25 @@ dotnet add package Microsoft.Extensions.Caching.SqlServer A concrete implementation of the `HybridCache` abstract class is included in the shared framework and is provided via dependency injection. But developers are welcome to provide or consume custom implementations of the API, for example [FusionCache](https://github.com/ZiggyCreatures/FusionCache/blob/main/docs/MicrosoftHybridCache.md). +## Use Hybrid Cache with Native AOT + + The following Native AOT-specific considerations apply to `HybridCache`: + +* **Serialization** + + Native AOT doesn't support runtime reflection-based serialization. If you cache custom types, you must use source generators or explicitly configure serializers that are compatible with AOT, like `System.Text.Json` source generation. For example, with `System.Text.Json`, register a `JsonSerializerContext` for your types and configure it in `AddHybridCache`. + +* **Trimming** + + Make sure all types you cache are referenced in a way that prevents them from being trimmed by the AOT compiler. Using source generators for serialization helps with this requirement. For more information, see . + +So the main difference if you want to use AOT with `AddHybridCache` is the need to ensure that your serialization setup is AOT-compatible. Otherwise, `HybridCache` works the same way in Native AOT as in regular ASP.NET Core apps. + + ## Compatibility The `HybridCache` library supports older .NET runtimes, down to .NET Framework 4.7.2 and .NET Standard 2.0. ## Additional resources -For more information about `HybridCache`, see the following resources: - -* GitHub issue [dotnet/aspnetcore #54647](https://github.com/dotnet/aspnetcore/issues/54647). -* [`HybridCache` source code](https://source.dot.net/#Microsoft.Extensions.Caching.Abstractions/Hybrid/HybridCache.cs,8c0fe94693d1ac8d) +For more information, see [the `HybridCache` source code](https://source.dot.net/#Microsoft.Extensions.Caching.Abstractions/Hybrid/HybridCache.cs) From 69ff51fde14e3049327e96dcc976dd762fcdf10d Mon Sep 17 00:00:00 2001 From: Tom Dykstra Date: Wed, 21 May 2025 12:03:23 -0700 Subject: [PATCH 02/10] proofread --- aspnetcore/performance/caching/hybrid.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aspnetcore/performance/caching/hybrid.md b/aspnetcore/performance/caching/hybrid.md index 43cecaeefa57..5bdfffc613f0 100644 --- a/aspnetcore/performance/caching/hybrid.md +++ b/aspnetcore/performance/caching/hybrid.md @@ -65,7 +65,7 @@ cache.GetOrCreateAsync($"user_prefs_{userId}", ...); It's the caller's responsibility to ensure that a key scheme is valid and can't cause data to become confused. -Avoid using external user input directly in cache keys. For example, don't use raw strings from user interfaces as cache keys. Doing so can expose your app to security risks, such as unauthorized access or denial-of-service attacks caused by flooding the cache with random or meaningless keys. In the valid examples above, the *order* and *user preference* data are clearly separated and use trusted identifiers: +Avoid using external user input directly in cache keys. For example, don't use raw strings from user interfaces as cache keys. Doing so can expose your app to security risks, such as unauthorized access or denial-of-service attacks caused by flooding the cache with random or meaningless keys. In the preceding valid examples, the *order* and *user preference* data are clearly separated and use trusted identifiers: * `orderid` and `userId` are internally generated identifiers. * `region` might be an enum or string from a predefined list of known regions. From c067fd5b413e23b54906ab583b22b5091009c62e Mon Sep 17 00:00:00 2001 From: Tom Dykstra Date: Wed, 21 May 2025 16:07:45 -0700 Subject: [PATCH 03/10] copilot --- aspnetcore/performance/caching/hybrid.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/aspnetcore/performance/caching/hybrid.md b/aspnetcore/performance/caching/hybrid.md index 5bdfffc613f0..1ddfc431d9e7 100644 --- a/aspnetcore/performance/caching/hybrid.md +++ b/aspnetcore/performance/caching/hybrid.md @@ -242,8 +242,7 @@ A concrete implementation of the `HybridCache` abstract class is included in the Make sure all types you cache are referenced in a way that prevents them from being trimmed by the AOT compiler. Using source generators for serialization helps with this requirement. For more information, see . -So the main difference if you want to use AOT with `AddHybridCache` is the need to ensure that your serialization setup is AOT-compatible. Otherwise, `HybridCache` works the same way in Native AOT as in regular ASP.NET Core apps. - +If you set up serialization and trimming correctly, `HybridCache` behaves the sme way in Native AOT as in regular ASP.NET Core apps. ## Compatibility From b97b2ef4d501134ae2e259e469409606dc8e8db1 Mon Sep 17 00:00:00 2001 From: Tom Dykstra Date: Wed, 21 May 2025 16:36:36 -0700 Subject: [PATCH 04/10] clarify some text --- aspnetcore/performance/caching/hybrid.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aspnetcore/performance/caching/hybrid.md b/aspnetcore/performance/caching/hybrid.md index 1ddfc431d9e7..32506f25f2da 100644 --- a/aspnetcore/performance/caching/hybrid.md +++ b/aspnetcore/performance/caching/hybrid.md @@ -208,7 +208,7 @@ Because much `HybridCache` usage will be adapted from existing `IDistributedCach * They're immutable types. * The code doesn't modify them. -In such cases, inform `HybridCache` that it's safe to reuse instances by: +In such cases, inform `HybridCache` that it's safe to reuse instances by making at least one of the following changes: * Marking the type as `sealed`. The `sealed` keyword in C# means that the class can't be inherited. * Applying the `[ImmutableObject(true)]` attribute to the type. The `[ImmutableObject(true)]` attribute indicates that the object's state can't be changed after it's created. From f55bb7d532497ad4afe5e54063b4b4c9f3bb8dac Mon Sep 17 00:00:00 2001 From: Tom Dykstra Date: Thu, 22 May 2025 16:42:27 -0700 Subject: [PATCH 05/10] ms.date --- aspnetcore/performance/caching/hybrid.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aspnetcore/performance/caching/hybrid.md b/aspnetcore/performance/caching/hybrid.md index 32506f25f2da..c3af26d3ccf2 100644 --- a/aspnetcore/performance/caching/hybrid.md +++ b/aspnetcore/performance/caching/hybrid.md @@ -4,7 +4,7 @@ author: tdykstra description: Learn how to use HybridCache library in ASP.NET Core. monikerRange: '>= aspnetcore-9.0' ms.author: tdykstra -ms.date: 07/16/2024 +ms.date: 05/22/2025 uid: performance/caching/hybrid --- # HybridCache library in ASP.NET Core From 868f021b9965bc24ba171abafe0b49f0de1433e4 Mon Sep 17 00:00:00 2001 From: Tom Dykstra Date: Sun, 25 May 2025 11:36:10 -0700 Subject: [PATCH 06/10] Update aspnetcore/performance/caching/hybrid.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- aspnetcore/performance/caching/hybrid.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aspnetcore/performance/caching/hybrid.md b/aspnetcore/performance/caching/hybrid.md index c3af26d3ccf2..8382cdf6887a 100644 --- a/aspnetcore/performance/caching/hybrid.md +++ b/aspnetcore/performance/caching/hybrid.md @@ -242,7 +242,7 @@ A concrete implementation of the `HybridCache` abstract class is included in the Make sure all types you cache are referenced in a way that prevents them from being trimmed by the AOT compiler. Using source generators for serialization helps with this requirement. For more information, see . -If you set up serialization and trimming correctly, `HybridCache` behaves the sme way in Native AOT as in regular ASP.NET Core apps. +If you set up serialization and trimming correctly, `HybridCache` behaves the same way in Native AOT as in regular ASP.NET Core apps. ## Compatibility From e5cefa5ce3be372e357191c68d5102821eecd8a6 Mon Sep 17 00:00:00 2001 From: Tom Dykstra Date: Sun, 25 May 2025 12:14:47 -0700 Subject: [PATCH 07/10] ai-ssist --- aspnetcore/performance/caching/hybrid.md | 1 + 1 file changed, 1 insertion(+) diff --git a/aspnetcore/performance/caching/hybrid.md b/aspnetcore/performance/caching/hybrid.md index 8382cdf6887a..5383c03fdc33 100644 --- a/aspnetcore/performance/caching/hybrid.md +++ b/aspnetcore/performance/caching/hybrid.md @@ -6,6 +6,7 @@ monikerRange: '>= aspnetcore-9.0' ms.author: tdykstra ms.date: 05/22/2025 uid: performance/caching/hybrid +ms.ai: assisted --- # HybridCache library in ASP.NET Core From 14ece13f19a28cfd333ff5ed31725a99bb2891f7 Mon Sep 17 00:00:00 2001 From: Tom Dykstra Date: Wed, 28 May 2025 17:40:50 -0700 Subject: [PATCH 08/10] update package reff --- .../caching/hybrid/samples/9.x/HCMinimal/HCMinimal.csproj | 2 +- .../hybrid/samples/9.x/HCMinimal2/HCMinimal.csproj | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/aspnetcore/performance/caching/hybrid/samples/9.x/HCMinimal/HCMinimal.csproj b/aspnetcore/performance/caching/hybrid/samples/9.x/HCMinimal/HCMinimal.csproj index 9a6b8e185674..73191c8ea1e5 100644 --- a/aspnetcore/performance/caching/hybrid/samples/9.x/HCMinimal/HCMinimal.csproj +++ b/aspnetcore/performance/caching/hybrid/samples/9.x/HCMinimal/HCMinimal.csproj @@ -7,7 +7,7 @@ - + diff --git a/aspnetcore/performance/caching/hybrid/samples/9.x/HCMinimal2/HCMinimal.csproj b/aspnetcore/performance/caching/hybrid/samples/9.x/HCMinimal2/HCMinimal.csproj index 1c7c988784d4..be31fb6a55d4 100644 --- a/aspnetcore/performance/caching/hybrid/samples/9.x/HCMinimal2/HCMinimal.csproj +++ b/aspnetcore/performance/caching/hybrid/samples/9.x/HCMinimal2/HCMinimal.csproj @@ -22,10 +22,10 @@ - - - - + + + + From 79d2a9764dcb1d8acc4a4ffccc56ebf96c209cee Mon Sep 17 00:00:00 2001 From: Tom Dykstra Date: Mon, 2 Jun 2025 15:23:32 -0700 Subject: [PATCH 09/10] aot and pkg version corections --- aspnetcore/performance/caching/hybrid.md | 2 +- .../caching/hybrid/samples/9.x/HCMinimal/HCMinimal.csproj | 2 +- .../hybrid/samples/9.x/HCMinimal2/HCMinimal.csproj | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/aspnetcore/performance/caching/hybrid.md b/aspnetcore/performance/caching/hybrid.md index 5383c03fdc33..b9410a5ac685 100644 --- a/aspnetcore/performance/caching/hybrid.md +++ b/aspnetcore/performance/caching/hybrid.md @@ -237,7 +237,7 @@ A concrete implementation of the `HybridCache` abstract class is included in the * **Serialization** - Native AOT doesn't support runtime reflection-based serialization. If you cache custom types, you must use source generators or explicitly configure serializers that are compatible with AOT, like `System.Text.Json` source generation. For example, with `System.Text.Json`, register a `JsonSerializerContext` for your types and configure it in `AddHybridCache`. + Native AOT doesn't support runtime reflection-based serialization. If you cache custom types, you must use source generators or explicitly configure serializers that are compatible with AOT, like `System.Text.Json` source generation. `HybridCache` is still under development, and simplifying the way to use it with AOT is a high priority for that development. For more information, see pull request [dotnet/extensions#6475](https://github.com/dotnet/extensions/pull/6475) * **Trimming** diff --git a/aspnetcore/performance/caching/hybrid/samples/9.x/HCMinimal/HCMinimal.csproj b/aspnetcore/performance/caching/hybrid/samples/9.x/HCMinimal/HCMinimal.csproj index 73191c8ea1e5..6dab0fde8a7f 100644 --- a/aspnetcore/performance/caching/hybrid/samples/9.x/HCMinimal/HCMinimal.csproj +++ b/aspnetcore/performance/caching/hybrid/samples/9.x/HCMinimal/HCMinimal.csproj @@ -7,7 +7,7 @@ - + diff --git a/aspnetcore/performance/caching/hybrid/samples/9.x/HCMinimal2/HCMinimal.csproj b/aspnetcore/performance/caching/hybrid/samples/9.x/HCMinimal2/HCMinimal.csproj index be31fb6a55d4..3d9807d30a31 100644 --- a/aspnetcore/performance/caching/hybrid/samples/9.x/HCMinimal2/HCMinimal.csproj +++ b/aspnetcore/performance/caching/hybrid/samples/9.x/HCMinimal2/HCMinimal.csproj @@ -22,10 +22,10 @@ - - - - + + + + From ec84a48fed0be0b7552b265de7b849f224fd1ae2 Mon Sep 17 00:00:00 2001 From: Tom Dykstra Date: Mon, 2 Jun 2025 15:27:28 -0700 Subject: [PATCH 10/10] Update aspnetcore/performance/caching/hybrid.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- aspnetcore/performance/caching/hybrid.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aspnetcore/performance/caching/hybrid.md b/aspnetcore/performance/caching/hybrid.md index b9410a5ac685..0b38ce34d5f7 100644 --- a/aspnetcore/performance/caching/hybrid.md +++ b/aspnetcore/performance/caching/hybrid.md @@ -85,7 +85,7 @@ Notice that the inline interpolated string syntax (`$"..."` in the preceding exa ### Additional key considerations -* Keys can be restricted to valid maximum lengths. For example, the default `HybridCache` implementation (via `AddHybridCache(...)`) restricts keys to 1,024 characters by default. That number is configurable via `HybridCacheOptions.MaximumKeyLength`, with longer keys bypassing the cache mechanisms to prevent saturation. +* Keys can be restricted to valid maximum lengths. For example, the default `HybridCache` implementation (via `AddHybridCache(...)`) restricts keys to 1024 characters by default. That number is configurable via `HybridCacheOptions.MaximumKeyLength`, with longer keys bypassing the cache mechanisms to prevent saturation. * Keys must be valid Unicode sequences. If invalid Unicode sequences are passed, the behavior is undefined. * When using an out-of-process secondary cache such as `IDistributedCache`, the backend implementation may impose additional restrictions. As a hypothetical example, a particular backend might use case-insensitive key logic. The default `HybridCache` (via `AddHybridCache(...)`) detects this scenario to prevent confusion attacks or alias attacks (using bitwise string equality). However, this scenario might still result in conflicting keys becoming overwritten or evicted sooner than expected.