Skip to content

Commit 5d2fb6b

Browse files
authored
HybridCache doc -- Add AOT, explain immutable, and edit pass using Acrolinx (#35508)
1 parent 26eec4a commit 5d2fb6b

File tree

3 files changed

+33
-21
lines changed

3 files changed

+33
-21
lines changed

aspnetcore/performance/caching/hybrid.md

Lines changed: 28 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@ author: tdykstra
44
description: Learn how to use HybridCache library in ASP.NET Core.
55
monikerRange: '>= aspnetcore-9.0'
66
ms.author: tdykstra
7-
ms.date: 07/16/2024
7+
ms.date: 05/22/2025
88
uid: performance/caching/hybrid
9+
ms.ai: assisted
910
---
1011
# HybridCache library in ASP.NET Core
1112

@@ -57,26 +58,26 @@ Both types of uniqueness are usually ensured by using string concatenation to ma
5758
cache.GetOrCreateAsync($"/orders/{region}/{orderId}", ...);
5859
```
5960

60-
or
61+
Or
6162

6263
```csharp
6364
cache.GetOrCreateAsync($"user_prefs_{userId}", ...);
6465
```
6566

6667
It's the caller's responsibility to ensure that a key scheme is valid and can't cause data to become confused.
6768

68-
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:
69+
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:
6970

7071
* `orderid` and `userId` are internally generated identifiers.
7172
* `region` might be an enum or string from a predefined list of known regions.
7273

73-
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
74+
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
7475
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:
7576

7677
* `customerId` 42 with `orderId` 123
7778
* `customerId` 421 with `orderId` 23
7879

79-
(both of which would generate the cache key `order42123`)
80+
Both of the preceding examples would generate the cache key `order42123`.
8081

8182
This guidance applies equally to any `string`-based cache API, such as `HybridCache`, `IDistributedCache`, and `IMemoryCache`.
8283

@@ -86,11 +87,11 @@ Notice that the inline interpolated string syntax (`$"..."` in the preceding exa
8687

8788
* 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.
8889
* Keys must be valid Unicode sequences. If invalid Unicode sequences are passed, the behavior is undefined.
89-
* 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.
90+
* 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.
9091

9192
### The alternative `GetOrCreateAsync` overload
9293

93-
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:
94+
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:
9495

9596
:::code language="csharp" source="~/performance/caching/hybrid/samples/9.x/HCMinimal/Program.cs" id="snippet_getorcreatestate" highlight="5-14":::
9697

@@ -117,11 +118,11 @@ Set tags when calling `GetOrCreateAsync`, as shown in the following example:
117118

118119
Remove all entries for a specified tag by calling <xref:Microsoft.Extensions.Caching.Hybrid.HybridCache.RemoveByTagAsync%2A> with the tag value. An overload lets you specify a collection of tag values.
119120

120-
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.
121+
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.
121122

122123
## Removing all cache entries
123124

124-
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`.
125+
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`.
125126

126127
### Additional tag considerations
127128

@@ -191,7 +192,7 @@ For more information, see the [HybridCache serialization sample app](https://git
191192
By default `HybridCache` uses <xref:System.Runtime.Caching.MemoryCache> 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).
192193

193194
> [!NOTE]
194-
> 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.
195+
> 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.
195196
196197
## Optimize performance
197198

@@ -205,10 +206,10 @@ In typical existing code that uses `IDistributedCache`, every retrieval of an ob
205206

206207
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:
207208

208-
* They are immutable types.
209+
* They're immutable types.
209210
* The code doesn't modify them.
210211

211-
In such cases, inform `HybridCache` that it's safe to reuse instances by:
212+
In such cases, inform `HybridCache` that it's safe to reuse instances by making at least one of the following changes:
212213

213214
* Marking the type as `sealed`. The `sealed` keyword in C# means that the class can't be inherited.
214215
* 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.
@@ -230,13 +231,24 @@ dotnet add package Microsoft.Extensions.Caching.SqlServer
230231

231232
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).
232233

234+
## Use Hybrid Cache with Native AOT
235+
236+
The following Native AOT-specific considerations apply to `HybridCache`:
237+
238+
* **Serialization**
239+
240+
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)
241+
242+
* **Trimming**
243+
244+
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 <xref:fundamentals/native-aot>.
245+
246+
If you set up serialization and trimming correctly, `HybridCache` behaves the same way in Native AOT as in regular ASP.NET Core apps.
247+
233248
## Compatibility
234249

235250
The `HybridCache` library supports older .NET runtimes, down to .NET Framework 4.7.2 and .NET Standard 2.0.
236251

237252
## Additional resources
238253

239-
For more information about `HybridCache`, see the following resources:
240-
241-
* GitHub issue [dotnet/aspnetcore #54647](https://github.com/dotnet/aspnetcore/issues/54647).
242-
* [`HybridCache` source code](https://source.dot.net/#Microsoft.Extensions.Caching.Abstractions/Hybrid/HybridCache.cs,8c0fe94693d1ac8d) <!--keep-->
254+
For more information, see [the `HybridCache` source code](https://source.dot.net/#Microsoft.Extensions.Caching.Abstractions/Hybrid/HybridCache.cs)

aspnetcore/performance/caching/hybrid/samples/9.x/HCMinimal/HCMinimal.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
</PropertyGroup>
88

99
<ItemGroup>
10-
<PackageReference Include="Microsoft.Extensions.Caching.Hybrid" Version="9.0.0-preview.7.24406.2" />
10+
<PackageReference Include="Microsoft.Extensions.Caching.Hybrid" Version="9.5.0" />
1111
</ItemGroup>
1212

1313
</Project>

aspnetcore/performance/caching/hybrid/samples/9.x/HCMinimal2/HCMinimal.csproj

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,10 @@
2222
</ItemGroup>
2323

2424
<ItemGroup>
25-
<PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="9.0.0-preview.7.24406.2" />
26-
<PackageReference Include="Microsoft.Extensions.Caching.SqlServer" Version="9.0.0-preview.7.24406.2" />
27-
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.0-preview.7.24406.2" />
28-
<PackageReference Include="Microsoft.Extensions.Caching.Hybrid" Version="9.0.0-preview.7.24406.2" />
25+
<PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="9.0.5" />
26+
<PackageReference Include="Microsoft.Extensions.Caching.SqlServer" Version="9.0.5" />
27+
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.5" />
28+
<PackageReference Include="Microsoft.Extensions.Caching.Hybrid" Version="9.5.0" />
2929
<PackageReference Include="Google.Protobuf" Version="3.26.1" />
3030
<PackageReference Include="Grpc.Tools" Version="2.63.0" />
3131
<PackageReference Include="Grpc.Core" Version="2.46.6" />

0 commit comments

Comments
 (0)