Skip to content

Cannot Access HttpContextAccessor inside hybridcache factory method #5648

Open

Description

@PouryaNoufallah96

Description

hello, i was trying the hybrid cache and noticed that when I'm inside the factory of the new hybridcache in GetOrCreateAsync and tried to access httpcontext but it was null.

this is when I'm inside the factory and try to access httpcontext to get user Identity or empty Guid and as you can see its empty
Image

and after I'm out of the cache in the same request I can access it and its filled:
Image

Reproduction Steps

injecting IHttpContextAccessor and then try to access it inside factory of GetOrCreateAsync

Expected behavior

i was expecting it to be filled current user identity

Actual behavior

and because httpcontextaccessor was null i get empty guid

Regression?

No response

Known Workarounds

No response

Configuration

No response

Other information

No response

Activity

  1. PouryaNoufallah96 commented on Nov 28, 2024

    @PouryaNoufallah96
    Author

    ?

  2. KalleOlaviNiemitalo commented on Dec 17, 2024

    @KalleOlaviNiemitalo

    I guess it's caused by the use of ThreadPool.UnsafeQueueWorkItem here:

    #if NETCOREAPP3_0_OR_GREATER
    ThreadPool.UnsafeQueueUserWorkItem(this, false);
    #else
    ThreadPool.UnsafeQueueUserWorkItem(SharedWaitCallback, this);
    #endif

    which is called from here:

    if (GetOrCreateStampedeState<TState, T>(key, flags, out var stampede, canBeCanceled))
    {
    // new query; we're responsible for making it happen
    if (canBeCanceled)
    {
    // *we* might cancel, but someone else might be depending on the result; start the
    // work independently, then we'll with join the outcome
    stampede.QueueUserWorkItem(in state, underlyingDataCallback, options);

    so the underlyingDataCallback gets called in a thread-pool work item to which the ExecutionContext does not propagate.


    In your application, I think you can work around it like this:

    var x = await cache.GetOrCreateAsync<ExecutionContext, Guid?>(
        key: "current-user",
        state: ExecutionContext.Capture(),
        factory: async (ExecutionContext executionContext, CancellationToken token) =>
            {
                Guid userId = default;
                ExecutionContext.Run(
                    executionContext,
                    callback: async (object state) =>
                    {
                        CancellationToken token = (CancellationToken)state;
                        await Task.Delay(10, token);
                        userId = userAccessor.GetUserIdentity;
                        Console.WriteLine(userId);
                    },
                    state: token);
                return userId;
            },
        options: …

    i.e. capture the ExecutionContext and make the factory restore it; but this will cost additional closure and delegate allocations.

  3. PouryaNoufallah96 commented on Dec 18, 2024

    @PouryaNoufallah96
    Author

    so i think its a bug. Currently im not using cache aside pattern for hybrid cache by setting the option like:
    options: new HybridCacheEntryOptions { Flags = HybridCacheEntryFlags.DisableUnderlyingData }
    so it only bring me the cached data

  4. nielsheeren commented on Apr 11, 2025

    @nielsheeren

    I made this work-around extension but I hope it will be fixed soon.

    public static class HybridCacheExtensions
    {
        
        // Work-around for hybrid cache not transporting the execution context (see: https://github.com/dotnet/extensions/issues/5648)
        internal static ValueTask<T> GetOrCreateInExecutionContextAsync<T>(
            this HybridCache cache,
            string key,
            Func<CancellationToken, ValueTask<T>> factory,
            HybridCacheEntryOptions? options = null,
            IEnumerable<string>? tags = null,
            CancellationToken cancellationToken = default
        ) => cache.GetOrCreateAsync(key, ExecutionContext.Capture(), (context, token) => { ExecutionContext.Restore(context); return factory(token); }, options, tags, cancellationToken);
    
        internal static ValueTask<T> GetOrCreateInExecutionContextAsync<T, TState>(
            this HybridCache cache,
            string key,
            TState state,
            Func<TState, CancellationToken, ValueTask<T>> factory,
            HybridCacheEntryOptions? options = null,
            IEnumerable<string>? tags = null,
            CancellationToken cancellationToken = default
        ) => cache.GetOrCreateAsync(key, (Context: ExecutionContext.Capture(), OriginalState: state), (state, token) => { ExecutionContext.Restore(state.Context); return factory(state.OriginalState, token); }, options, tags, cancellationToken);
    }
  5. BOBx5 commented on Jul 4, 2025

    @BOBx5

    I made this work-around extension but I hope it will be fixed soon.

    public static class HybridCacheExtensions
    {

    // Work-around for hybrid cache not transporting the execution context (see: https://github.com/dotnet/extensions/issues/5648)
    internal static ValueTask<T> GetOrCreateInExecutionContextAsync<T>(
        this HybridCache cache,
        string key,
        Func<CancellationToken, ValueTask<T>> factory,
        HybridCacheEntryOptions? options = null,
        IEnumerable<string>? tags = null,
        CancellationToken cancellationToken = default
    ) => cache.GetOrCreateAsync(key, ExecutionContext.Capture(), (context, token) => { ExecutionContext.Restore(context); return factory(token); }, options, tags, cancellationToken);
    
    internal static ValueTask<T> GetOrCreateInExecutionContextAsync<T, TState>(
        this HybridCache cache,
        string key,
        TState state,
        Func<TState, CancellationToken, ValueTask<T>> factory,
        HybridCacheEntryOptions? options = null,
        IEnumerable<string>? tags = null,
        CancellationToken cancellationToken = default
    ) => cache.GetOrCreateAsync(key, (Context: ExecutionContext.Capture(), OriginalState: state), (state, token) => { ExecutionContext.Restore(state.Context); return factory(state.OriginalState, token); }, options, tags, cancellationToken);
    

    }

    This approach kind of half works.
    When there is no cached data, It executes factory method and it works fine within correct ExecutionContext,
    But when only GET is executed, sadly, it doesn't work.
    Although, thanks to @nielsheeren

  6. jacobslusser commented on Sep 10, 2025

    @jacobslusser

    It's a shame this doesn't seem to be taken seriously. If the downstream work being done by the factory method is significant, this can at minimum: orphan a tremendous amount of telemetry breaking the call graph, at worse case: break the application which needs access to current request.

    EDIT: Telemetry doesn't flow because Activity.Current doesn't either.

  7. denisyfrolov commented on Dec 3, 2025

    @denisyfrolov

    +1 We really need this one to get fixed

  8. jodydonetti commented on Dec 8, 2025

    @jodydonetti
    Contributor

    Hi @denisyfrolov

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

    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.

  9. PeterRygeBestseller commented on Feb 25, 2026

    @PeterRygeBestseller

    +1
    I just ran into this bug as well. Would be nice if it gets fixed.

  10. altso commented on Mar 24, 2026

    @altso

    An alternative workaround is not to pass CancellationToken.

    if (canBeCanceled)
    {
    // *we* might cancel, but someone else might be depending on the result; start the
    // work independently, then we'll with join the outcome
    stampede.QueueUserWorkItem(in state, underlyingDataCallback, options);
    }
    else
    {
    // we're going to run to completion; no need to get complicated
    _ = stampede.ExecuteDirectAsync(in state, underlyingDataCallback, options); // this larger task includes L2 write etc
    return stampede.UnwrapReservedAsync(_logger);
    }

  11. altso commented on Mar 25, 2026

    @altso

    But when only GET is executed, sadly, it doesn't work.

    @BOBx5 can you please clarify?

  12. BOBx5 commented on Mar 26, 2026

    @BOBx5

    @altso

    Now I moved to ZiggyCreatures.FusionCache (thanks to @jodydonetti).

    Note

    I did run my API solution with .NET Aspire.
    In Aspire, I could see OpenTelemetry Traces.

    1. Register singleton CacheService which uses HybridCache from Microsoft.Extensions.Caching.Hybrid:
      // Program.cs
      builder.Services.AddSingleton<ICacheService, CacheService>();
      
      // CacheService.cs
      using Microsoft.Extensions.Caching.Hybrid;
      internal sealed class CacheService(HybridCache hybridCache) : ICacheService
      {
          // ...
      }
    2. Invoke HTTP request handler which uses ICacheService through DI.
      // Program.cs
      app.MapGet("/weatherforecast", (
          string city,
          ICacheService cacheService,
          CancellationToken cancellationToken = default) =>
      {
          return await cacheService.GetOrCreateAsync(
              key: city,
              factory: () => city switch
              {
                  "istanbul" => new WeatherForecast { City = city, TemperatureC = 25, Summary = "Sunny" },
                  "london" => new WeatherForecast { City = city, TemperatureC = 15, Summary = "Cloudy" },
                  _ => new WeatherForecast { City = city, TemperatureC = 20, Summary = "Unknown" }
              },
              cancelationToken: cancellationToken);
          )
      });
      • This action add a Trace row in Aspire dashboard.
        Image
      • Also initialize CacheService and add it to .NET DI resolver.
      • This will trigger command of Redis GET and SET operations.
    3. Now, Send another HTTP request to the same endpoint with the same city name.
      • This will trigger command of Redis GET operation only, and no SET operation.
      • Expected
        • Another trace is added and redis related traces should be appended on the second traces.
      • Actual
        • Second trace is added but Redis related traces are appended on the first trace, not the second one.
        • This unexpected behavior is observed, and it increase Duration column time span of the first trace, when it first one begins and end when the second one ends.
        • If I send more requests, the Duration column time span of the first trace will increase more and more.
  13. altso commented on Mar 26, 2026

    @altso

    Thank you @BOBx5! I really appreciate the detailed response.

  14. chertby commented on Oct 5, 2026

    @chertby

    Hi,

    Are there any updates or plans to address this issue?

    We are seeing the same behavior, but in our case it also affects OpenTelemetry trace propagation.

    We use HybridCache with Redis as the distributed (L2) cache. When GetOrCreateAsync enters the shared/stampede-protected work, Activity.Current is no longer propagated. As a result:

    • Redis operations performed internally by HybridCache (HGET/HMGET) start new traces instead of being children of the calling operation.
    • The factory callback also loses the original Activity.Current, so database operations executed from the factory are disconnected from the original trace.

    For the factory we can work around this by capturing Activity.Current.Context before calling GetOrCreateAsync, passing the ActivityContext through TState, and explicitly creating an activity with that parent inside the factory.

    However, this cannot fix the L2 Redis operation because it happens internally in HybridCache before the factory is invoked.

    This results in traces roughly like:

    original trace
      └─ application cache operation
    
    separate trace
      └─ Redis HMGET
    

    while both operations are part of the same logical GetOrCreateAsync call.

    Is preserving tracing context (or otherwise providing OpenTelemetry-friendly correlation for the shared HybridCache operation) something that is planned to be addressed?

    Thanks!

  15. added this to the 12.0 milestone on Oct 7, 2026
  16. svick commented on Oct 7, 2026

    @svick
    Member

    @chertby I don't think we'll be able to fix this in .Net 11 (it's too close to release), but I want to look at this for .Net 12.

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

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions