Skip to content

HybridCache Preserve Activity in Factory Callback #6543

Closed as duplicate
Closed as duplicate

Description

@almarcus

Description

When using HybridCache and there is a cache miss on GetOrCreateAsync, the factory method is called but the parent Activity is not preserved and the factory method call has an event that is emitted with a different Trace ID

Reproduction Steps

var result = await cache.GetOrCreateAsync(
    "my-key",
    async ct =>
    {
        return await dbContext.MyTable.ToListAsync(ct); // emits its own span with a different trace ID
    },
    cancellationToken: cancellationToken);

Expected behavior

Trace ID of the factory method should be the same as the parent activity. This screenshot shows the trace without the use of HybridCache:

Image

Image

Actual behavior

Trace ID is different than the parent activity

Image

Image

Regression?

Unsure

Known Workarounds

Setting the Activity.Current to the outer Activity.Current within the factory method works, but isn't feasible or reasonable to do everywhere and should work with the default implementation of HybridCache:

var parentActivity = Activity.Current; // get parent activity

await cache.GetOrCreateAsync(
    "my-key",
    async ct =>
    {
        Activity.Current = parentActivity; // set current activity to parent

        return await dbContext.MyTable.ToListAsync(ct);
    });

Configuration

.NET SDK:
 Version:           9.0.200
 Commit:            90e8b202f2
 Workload version:  9.0.200-manifests.a8e064d4
 MSBuild version:   17.13.8+cbc39bea8

Runtime Environment:
 OS Name:     Mac OS X
 OS Version:  15.5
 OS Platform: Darwin
 RID:         osx-arm64
 Base Path:   /usr/local/share/dotnet/sdk/9.0.200/

.NET workloads installed:
There are no installed workloads to display.
Configured to use loose manifests when installing new manifests.

Host:
  Version:      10.0.0-preview.4.25258.110
  Architecture: arm64
  Commit:       c22dcd0c7a

.NET SDKs installed:
  8.0.402 [/usr/local/share/dotnet/sdk]
  9.0.100-rc.2.24474.11 [/usr/local/share/dotnet/sdk]
  9.0.100 [/usr/local/share/dotnet/sdk]
  9.0.102 [/usr/local/share/dotnet/sdk]
  9.0.200 [/usr/local/share/dotnet/sdk]
  10.0.100-preview.3.25201.16 [/usr/local/share/dotnet/sdk]
  10.0.100-preview.4.25258.110 [/usr/local/share/dotnet/sdk]

.NET runtimes installed:
  Microsoft.AspNetCore.App 8.0.8 [/usr/local/share/dotnet/shared/Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 9.0.0-rc.2.24474.3 [/usr/local/share/dotnet/shared/Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 9.0.0 [/usr/local/share/dotnet/shared/Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 9.0.1 [/usr/local/share/dotnet/shared/Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 9.0.2 [/usr/local/share/dotnet/shared/Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 10.0.0-preview.3.25172.1 [/usr/local/share/dotnet/shared/Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 10.0.0-preview.4.25258.110 [/usr/local/share/dotnet/shared/Microsoft.AspNetCore.App]
  Microsoft.NETCore.App 8.0.8 [/usr/local/share/dotnet/shared/Microsoft.NETCore.App]
  Microsoft.NETCore.App 9.0.0-rc.2.24473.5 [/usr/local/share/dotnet/shared/Microsoft.NETCore.App]
  Microsoft.NETCore.App 9.0.0 [/usr/local/share/dotnet/shared/Microsoft.NETCore.App]
  Microsoft.NETCore.App 9.0.1 [/usr/local/share/dotnet/shared/Microsoft.NETCore.App]
  Microsoft.NETCore.App 9.0.2 [/usr/local/share/dotnet/shared/Microsoft.NETCore.App]
  Microsoft.NETCore.App 10.0.0-preview.3.25171.5 [/usr/local/share/dotnet/shared/Microsoft.NETCore.App]
  Microsoft.NETCore.App 10.0.0-preview.4.25258.110 [/usr/local/share/dotnet/shared/Microsoft.NETCore.App]

Packages:

   > Microsoft.Extensions.Caching.Hybrid              9.6.0           9.6.0        
   > Microsoft.Extensions.Http.Resilience                   9.5.0           9.5.0        
   > OpenTelemetry.Exporter.OpenTelemetryProtocol           1.12.0          1.12.0       
   > OpenTelemetry.Extensions.Hosting                       1.12.0          1.12.0       
   > OpenTelemetry.Instrumentation.AspNetCore               1.12.0          1.12.0       
   > OpenTelemetry.Instrumentation.EntityFrameworkCore      1.12.0-beta.2   1.12.0-beta.2
   > OpenTelemetry.Instrumentation.Http                     1.12.0          1.12.0       
   > OpenTelemetry.Instrumentation.Runtime                  1.12.0          1.12.0       
   > Serilog.AspNetCore                                     9.0.0           9.0.0        
   > Serilog.Sinks.OpenTelemetry                            4.1.1           4.1.1      

Other information

No response

Activity

  1. added
    bugThis issue describes a behavior which is not expected - a bug.
    on Jun 26, 2025
  2. KalleOlaviNiemitalo commented on Jun 27, 2025

    @KalleOlaviNiemitalo

    This is caused by the same ExecutionContext / AsyncLocal<T> propagation problem as #5568 and #5648.

  3. jodydonetti commented on Dec 8, 2025

    @jodydonetti
    Contributor

    As a (temporary?) fix you may look into using the FusionCache based implementation of HybridCache, see here.

    It has full support for OTEL, see here.

    This is a result with the hierarchy respected:

    Image

    It also has a backplane to keep the cache coherent even in a multi-node environment, and that also works with the HybridCache impl.

    It's a 100% compatible drop-in impl replacement, no code changes required except for 1 line during setup.

    Hope this helps.

    NB: FusionCache creator here, just to be clear.

  4. dmitry-azaraev commented on Jan 28, 2026

    @dmitry-azaraev

    As a (temporary?) fix you may look into using the FusionCache based implementation of HybridCache, see here.
    It's a 100% compatible drop-in impl replacement, no code changes required except for 1 line during setup.

    Unfortunately it is not always so easy: FusionCache completely ignores IHybridCacheSerializerFactory-ies and IHybridCacheSerializer<T> as well. Additionally it exposes own IFusionCacheSerializer which initially looks simple, but ends what implementation must implement serialization of FusionCacheDistributedEntry<T> instead of T serializers and may be something else, implementing own entry framing. So... this simply doesn't work out of box in environment, where types has multipath serialization and serializable types are strictly enforced (e.g. no default serializer allowed). It also known nothing about IBufferDistributedCache which also might affect compatibility with something. So, yes, it is offer HybridCache interface, but it is far away from "100% compatible drop-in impl replacement".

    PS: FusionCache still featureful and nice. But serialization is suck with sucky interface.

  5. jodydonetti commented on Jan 29, 2026

    @jodydonetti
    Contributor

    Hi @dmitry-azaraev

    Unfortunately it is not always so easy: FusionCache completely ignores IHybridCacheSerializerFactory-ies and IHybridCacheSerializer<T> as well.

    Correct, the FusionCache adapter is 100% compatible with the HybridCache abstraction (public abstract class HybridCache), and not the default HybridCache impl (internal class DefaultHybridCache).

    The serializers abstractions are specific for the default implementation, not for the abstraction.

    Now, I admit that the serializers are kind of "tangential", meaning they are not part of the public HybridCache abstraction but some may think about them as part of the overall "contract".

    But as soon as you think in terms of DI component registrations, I think everything becomes clearer: for example would you expect the same logger (ILogger<HybridCache>) to be used? probably not, I think.

    Overall the main point is that the DI registration is the FusionCache one, not the HybridCache one: thanks to this I can support extra things like fail-safe, factory timeouts, the backplane, auto-recovery, etc.

    Basically the adapter is a way of saying "you can take a FusionCache registration (setup, config, options, etc) and use that as HybridCache".

    Maybe the wording could be different? Any idea?

    Additionally it exposes own IFusionCacheSerializer which initially looks simple, but ends what implementation must implement serialization of FusionCacheDistributedEntry<T> instead of T serializers and may be something else, implementing own entry framing.

    Yes, the entire entry (where entry = value + extra data/metadata) is serialized: 99.999% it works without issues, but I can see your friction in (I suppose) implementing a custom serializer.

    FWIW I've recently started thinking about moving in the future (v3?) to a different approach, similar to the default HybridCache impl.

    What I mean is like a combination of a header + value, where:

    • header: a fixed, internally encoded initial part with the needed info/metadata (not dependent on the selected serializer)
    • value: the cache value, serialized with the selected serializer

    There are pros/cons/tradeoffs to consider and it's not as straightforward as it may look like, mainly because of edge cases (evolving schemas, backward/forward compatibility, etc), but I'm exploring this.

    Also, I'm exploring adding direct (optional) support for IHybridCacheSerializer, so users may just say "use them kthxbye".

    If you have opinios or xperiences to share they would be welcome.

    So... this simply doesn't work out of box in environment, where types has multipath serialization and serializable types are strictly enforced (e.g. no default serializer allowed).

    Yeah well... multipath serialization is (imho, of course) not the best idea: it's way better to have different caches (like named caches) where you can more clearly split different setups and configs. Mixing things at the serialization level is... I don't know, more error prone (again, imho).

    It also known nothing about IBufferDistributedCache which also might affect compatibility with something.

    I'd like to add support for IBufferDistributedCache as soon as I have time, but:

    • I can't see compatibility being an issue, it's an opt-in optimization, but maybe I'm missing something
    • not many implementations added support for it

    PS: FusionCache still featureful and nice.

    Thanks.

    But serialization is suck with sucky interface.

    Ok 🤷‍♂️, if you'd like to expand I'm interested in knowing more.

    Btw, thanks for sharing, appreciate it!

    PS: even though some people may find the back and forth interesting, I'd like not to bother others in this repo. So if you would like to keep talking about this (FWIW: I would be interested), maybe we can create a new discussion over the FusionCache repo and just put a link here, what do you think?

  6. dmitry-azaraev commented on Jan 29, 2026

    @dmitry-azaraev

    @jodydonetti this definitely offtopic, but I'm reply just here but will try be short. Sorry, if I'm miss something important. Also, please, sorry, my previous message has been unnecessarily cruel.

    As for serializatoin interfaces and part of abstraction - they landed in abstractions assembly near HybridCache, they also present in official documentation, which teach how to use them. So, I'm assume they are part of this concept generally, however, technically, I agreed, what they part of default implementation.

    PS: even though some people may find the back and forth interesting, I'd like not to bother others in this repo. So if you would like to keep talking about this (FWIW: I would be interested), maybe we can create a new discussion over the FusionCache repo and just put a link here, what do you think?

    Let's skip any discussion for now, I'm will come eventually into your repository directly, if it will be needed. As for now, for me is better write serialization adapter to integrate them as I want... after few thoughts, I'm understand what I'm disappointed in HybridCache, but it is impractically go away from it right now, but this would be possible after some time. I think it should be nice addition to have such hybrid serialization adapter in FusionCache arsenal to make migration more easier. [I come here by ExecutionContext, but I'm miss not only this.]

    FC's serialization framing/header is definitely can be done in HybridCache-way, e.g. when header maintained by library itself, but I see advantage of current FusionCache-way too, because JSON/other payloads doesn't get mixed with different encodings, so they can be easier inspect, by single decoder (however, I'm guess it is not what everyone do).

    Serialization interface is "suck" because it lacks of clear documentation (or I'm doesn't found it, I'm tried but probably tired and miss that), surely there is exist reference implementation(s), and generally it is easy, but still interface by itself nowhere say what it always would do byte[] Serialize<T>(FusionCacheDistributedEntry<T?> obj). Also byte[] allocations are generally problematic for me, so I'm prefer avoid them, but to be fair: in real context for me where cache is used - their cost neglible (however in same software system in major part of it (physically divided) similar naive approach leads to significant performance issues, so it just like act red flag on me, but i can live with it). But still new version can do everything even better. :)

    Thank you!

  7. SylvainRollinetSoftcom commented on Mar 16, 2026

    @SylvainRollinetSoftcom

    Just got the same issue today...

    It seems it only happens if a cancellation token is passed. So one workaround is to omit it, if acceptable.

  8. locked and limited conversation to collaborators on Aug 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area-caching-hybridbugThis issue describes a behavior which is not expected - a bug.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions