dotnet / aspnetcore

ASP.NET Core is a cross-platform .NET framework for building modern cloud-based web applications on Windows, Mac, or Linux.

Home Page:https://asp.net

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

HttpContext property of HttpContextAccessor is null

cosmin-ciuc opened this issue · comments

httpcontextaccessor_problem
I have a REST API web application which worked perfectly with ASP.Net Core 2.1. Now the constructor injected IHttpContextAccessor returns a null value for HttpContext property.

In Startup.cs class, in ConfigureServices method I have:

services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();          
services.AddScoped<WorklistPagedTasksHalRepresentationConverter>();

This is the WorklistPagedTasksHalRepresentationConverter class

public class WorklistPagedTasksHalRepresentationConverter : TypeConverter<WorklistWithPagedTasks, WorklistPagedTasksHalRepresentation>
    {
        private readonly IHttpContextAccessor httpContextAccessor;

        private readonly IMapper mapper;

        public WorklistPagedTasksHalRepresentationConverter(IHttpContextAccessor httpContextAccessor, IMapper mapper)
        {
            this.httpContextAccessor = httpContextAccessor;
            this.mapper = mapper;
        }

        public WorklistPagedTasksHalRepresentation Convert(WorklistWithPagedTasks source, WorklistPagedTasksHalRepresentation destination, ResolutionContext context)
        {
            var tasks = mapper.Map<IList<TaskSummaryHalRepresentation>>(source.Tasks.ResultPage.ToList());
            var worklistPagedTasksHalRepresentation = new WorklistPagedTasksHalRepresentation(
                new PaginatedResult<TaskSummaryHalRepresentation>
                {
                    ResultPage = tasks.AsQueryable(),
                    TotalCount = source.Tasks.TotalCount,
                    PageNumber = source.Tasks.PageNumber
                },
                httpContextAccessor.HttpContext.Request)
            {
                Id = source.Id,
                OwnerId = source.OwnerId,
                OwnerName = source.OwnerName
            };
            return worklistPagedTasksHalRepresentation;
        }
    }

The Convert method is called by a controller method.

Can you show your controller method? The one that calls this convert method?

Sure I can post the controller method calling the Convert method but it is a little bit more complicated than just that since I'm using Automapper Nuget package to convert from one type instance to another and the Automapper is the one that is actually calling the Convert method.

I was planning to create a simplified project that could be used to replicate this issue.

You need to have the following Nuget package reference:
<PackageReference Include="AutoMapper" Version="8.0.0" />

Then you would need the following classes

   public class AutoMapperConfiguration
   {
        public static void Configure(IMapperConfigurationExpression config)
        {
           config.AddProfile<WorklistMappingProfile>();
        }
   }

    public class WorklistMappingProfile : AutoMapper.Profile
    {
        public WorklistMappingProfile()
        {
            CreateMap<WorklistWithPagedTasks, WorklistPagedTasksHalRepresentation>().ConvertUsing<WorklistPagedTasksHalRepresentationConverter>();
        }
    }

    public class WorklistSummary
    {
        public Guid Id { get; set; }

        public Guid? OwnerId { get; set; }

        public string OwnerName { get; set; }
    }

    public class WorklistWithPagedTasks : WorklistSummary
    {
        public PaginatedResult<TaskSummary> Tasks { get; set; }
    }

    public class PaginatedResult<TEntity>
    {
        public IQueryable<TEntity> ResultPage { get; set; }

        public int TotalCount { get; set; }

        public int PageNumber { get; set; }
    }

    [Serializable]
    public class BaseEntity
    {
        public Guid Id { get; set; }

        public string DisplayName { get; set; }

        public bool Active { get; set; }

        public string HelpTextHref { get; set; }
        
        public int Order { get; set; }
    }

    [Serializable]
    public class TaskSummary : BaseEntity
    {
        public TaskSummary()
        {
        }

        public TaskSummary(string taskLibraryEntryId)
        {
            TaskLibraryEntryId = taskLibraryEntryId;
        }

        public byte Priority { get; set; }

        public string Note { get; set; }

        public bool Reminder { get; set; }

        public DateTime DueAt { get; set; }

        public Guid? OwnerId { get; set; }

        public Guid? SenderId { get; set; }

        public string SenderName { get; set; }

        public string OwnerName { get; set; }
        
        public string SenderUsername { get; set; }

        public string OwnerUsername { get; set; }

        public string CaseId { get; set; }

        public bool Completed { get; set; }

        public string TaskLibraryEntryId { get; set; }

        public DateTime? Created { get; set; }

        public string TaskDefinitionId { get; set; }

        public bool Accessed { get; set; }
    }

    public class TaskSummaryHalRepresentation
    {
        protected readonly HttpRequest httpRequest;

        private string href;

        public TaskSummaryHalRepresentation(HttpRequest httpRequest)
        {
            this.httpRequest = httpRequest;
        }

        [JsonProperty("id")]
        public Guid Id { get; set; }

        [JsonProperty("caseId", NullValueHandling = NullValueHandling.Include)]
        public string CaseId { get; set; }

        [JsonProperty("displayName", NullValueHandling = NullValueHandling.Include)]
        public string DisplayName { get; set; }

        [JsonProperty("active", NullValueHandling = NullValueHandling.Include)]
        public bool Active { get; set; }

        [JsonProperty("dueAt", NullValueHandling = NullValueHandling.Include)]
        public DateTime DueAt { get; set; }

        [JsonProperty("note", NullValueHandling = NullValueHandling.Include)]
        public string Note { get; set; }

        [JsonProperty("priority", NullValueHandling = NullValueHandling.Include)]
        public int Priority { get; set; }

        [JsonProperty("reminder", NullValueHandling = NullValueHandling.Include)]
        public bool Reminder { get; set; }

        [JsonProperty("ownerName", NullValueHandling = NullValueHandling.Include)]
        public string OwnerName { get; set; }

        [JsonProperty("senderName", NullValueHandling = NullValueHandling.Include)]
        public string SenderName { get; set; }

        [JsonProperty("completed", NullValueHandling = NullValueHandling.Include)]
        public bool Completed { get; set; }

        [JsonIgnore]
        public string OwnerUserName { get; set; }

        [JsonIgnore]
        public string SenderUserName { get; set; }

        [JsonProperty("ownerId", NullValueHandling = NullValueHandling.Include)]
        public Guid? OwnerId { get; set; }

        [JsonProperty("senderId", NullValueHandling = NullValueHandling.Include)]
        public Guid? SenderId { get; set; }

        [JsonProperty("created", NullValueHandling = NullValueHandling.Include)]
        public DateTime? Created { get; set; }

        [JsonProperty("accessed")]
        public bool Accessed { get; set; }
        
        [JsonProperty("taskDefinitionId", NullValueHandling = NullValueHandling.Include)]
        public string TaskDefinitionId { get; set; }
    }

    public class HalRepresentationPagedList<TResource> where TResource : class
    {
        private readonly HttpRequest httpRequest;

        private double maxPage;

        public HalRepresentationPagedList(HttpRequest httpRequest)
        {
            ResourceList = new List<TResource>();
            this.httpRequest = httpRequest;
            Count = 0;
        }

        public HalRepresentationPagedList(PaginatedResult<TResource> paginatedResource, HttpRequest httpRequest) : this(httpRequest)
        {
            ResourceList = paginatedResource.ResultPage?.ToList() ?? new List<TResource>();
            Count = ResourceList.Count();
            Total = paginatedResource.TotalCount;
            Page = paginatedResource.PageNumber;
        }

        [JsonIgnore]
        public int Page { get; }

        [JsonIgnore]
        public int InitialCount { get; set; }

        [JsonIgnore]
        public bool HasDefaultCount { get; set; }

        [JsonIgnore]
        public int DefaultCount { get; set; }

        [JsonProperty("count")]
        public int Count { get; }

        [JsonProperty("pageSize")]
        public int PageSize { get; set; }

        [JsonProperty("total")]
        public int Total { get; }

        public virtual List<TResource> ResourceList { get; set; }

        [JsonIgnore]
        public List<KeyValuePair<string, string>> QueryParams { get; }
    }

    public sealed class WorklistPagedTasksHalRepresentation : HalRepresentationPagedList<TaskSummaryHalRepresentation>
    {
        public WorklistPagedTasksHalRepresentation(PaginatedResult<TaskSummaryHalRepresentation> paginatedTasks, HttpRequest httpRequest) : base(paginatedTasks, httpRequest)
        {
        }

        [JsonProperty("id")]
        public Guid Id { get; set; }

        [JsonProperty("ownerName", NullValueHandling = NullValueHandling.Include)]
        public string OwnerName { get; set; }

        [JsonProperty("ownerId", NullValueHandling = NullValueHandling.Include)]
        public Guid? OwnerId { get; set; }

        [JsonProperty("tasks")]
        public override List<TaskSummaryHalRepresentation> ResourceList { get; set; }
    }

    public class WorklistPagedTasksHalRepresentationConverter : ITypeConverter<WorklistWithPagedTasks, WorklistPagedTasksHalRepresentation>
    {
        private readonly IHttpContextAccessor httpContextAccessor;

        private readonly IMapper mapper;

        public WorklistPagedTasksHalRepresentationConverter(IHttpContextAccessor httpContextAccessor, IMapper mapper)
        {
            this.httpContextAccessor = httpContextAccessor;
            this.mapper = mapper;
        }

        public WorklistPagedTasksHalRepresentation Convert(WorklistWithPagedTasks source, WorklistPagedTasksHalRepresentation destination, ResolutionContext context)
        {
            var tasks = mapper.Map<IList<TaskSummaryHalRepresentation>>(source.Tasks.ResultPage.ToList());
            var worklistPagedTasksHalRepresentation = new WorklistPagedTasksHalRepresentation(
                new PaginatedResult<TaskSummaryHalRepresentation>
                {
                    ResultPage = tasks.AsQueryable(),
                    TotalCount = source.Tasks.TotalCount,
                    PageNumber = source.Tasks.PageNumber
                },
                httpContextAccessor.HttpContext.Request)
            {
                Id = source.Id,
                OwnerId = source.OwnerId,
                OwnerName = source.OwnerName
            };
            return worklistPagedTasksHalRepresentation;
        }
    }

    public class WorklistPostInput
    {
        public string OwnerUsername { get; set; }

        [JsonProperty("ownerId")]
        public string OwnerId { get; set; }
    }

After that in ConfigureServices method of your Startup class you need to add the following code:

services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddScoped<WorklistPagedTasksHalRepresentationConverter>();
Mapper.Initialize(AutoMapperConfiguration.Configure);
services.AddSingleton(Mapper.Configuration);
services.AddScoped<IMapper>(
    serviceProvider => new Mapper(serviceProvider.GetRequiredService<AutoMapper.IConfigurationProvider>(), serviceProvider.GetService));

And now the controller method:

    [ApiController]
    [Route("fs/rest/worklists")]
    [Produces("application/json")]
    public class WorklistsController : ControllerBase
    {
        private readonly IMapper mapper;

        public WorklistsController(IMapper mapper)
        {
            this.mapper = mapper;
        }

        [HttpPost("")]
        [ProducesResponseType(typeof(WorklistPagedTasksHalRepresentation), (int)HttpStatusCode.Created)]
        public async Task<ActionResult<WorklistPagedTasksHalRepresentation>> PostWorklistAsync([FromBody]WorklistPostInput worklist)
        {
            var newWorklist = new WorklistWithPagedTasks();
            if (worklist != null)
            {
                if (!string.IsNullOrWhiteSpace(worklist.OwnerId))
                {
                    Guid.TryParse(worklist.OwnerId, out var ownerid);
                    newWorklist.OwnerId = ownerid; 
                }
            }

            newWorklist.Tasks = new PaginatedResult<TaskSummary> { PageNumber = 1, ResultPage = new List<TaskSummary>().AsQueryable(), TotalCount = 0 };
            var worklistHalRepresentationPaged = mapper.Map<WorklistPagedTasksHalRepresentation>(newWorklist);
            return Created("myUrl", worklistHalRepresentationPaged);
        }
    }

I've done some more tests. It seems that if you alter the value of HttpContext.TraceIdentifier property in a middleware, the HttpContextAccessor.HttpContext property will become null.

I'm attaching a sample web application which can be used to reproduce the error. If you'll make the following HTTP request:

POST http://localhost:65448/api/worklists HTTP/1.1
X-Correlation-ID: 80000024-0007-fa00-b63f-84710c7967bc
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.140 Safari/537.36 Edge/17.17134
accept: application/json
content-type: application/json
Accept-Language: en-US,en;q=0.8,nb;q=0.6,ro-RO;q=0.4,ro;q=0.2
Accept-Encoding: gzip, deflate
Connection: Keep-Alive
Host: localhost:65448
Content-Length: 0

you'll reproduce the error.

WebApplication1.zip

@davidfowl: I have made another web application project, even simpler, that can be used to reproduce the problem. I am 100% convinced that if you alter the HttpContext.TraceIdentifier from the middleware code you force HttpContextAccessor instance to set its HttpContext property to null.

Please see the attached sample application.
WebApplication2.zip

If you call GET api/values without specifying the HTTP header X-Correlation-ID everything works fine. But if you make the following request:

GET http://localhost:60065/api/values HTTP/1.1
X-Correlation-ID: myCorrelationId
accept: application/json
Accept-Language: en-US,en;q=0.8,nb;q=0.6,ro-RO;q=0.4,ro;q=0.2
Host: localhost:60065

the HttpContext property of IHttpContextAccessor becomes null. This happens because in CurrentRequestMiddleware I'm setting the value of HttpContext.TraceIdentifier to the value of X-Correlation-ID header.

Thank you.

@cosmin-ciuc Thanks for investigating that - I've had exactly the same problem, but haven't had a chance to figure out why it's happening.

I've delayed our upgrade to 2.2 because of this exact problem.

I would try to fix this problem myself but it's going to take me a while until I familiarize myself with the projects and classes structure. I think it would be much more efficient if the developers of ASP.NET Core 2.2 would look into the issue and they would make a fix for it.

Yes this is a change made in 2.2. It’s fixed in 3.0, maybe we should backport that’s change to 2.2.x.

cc @muratg

@davidfowl where's the fix in master?

@davidfowl / @muratg We 100% need this fix back ported, please!

Here's the regression: 0f4f195

Here's the 3.0 fix: 49d785c

@davidfowl Are you able to send a 2.2.2 PR?

No I’m on vacation, have somebody backport the PR 😬

@davidfowl Since 2.2.1 is done, earliest this can go in is 2.2.2. Plenty of time in January to do it. Please make sure to fill out the shiproom template.

@JunTaoLuo Could you prepare this for 2.2.2 please?

Work is backporting Ben's PR from master and filling out the shiproom template.

@JunTaoLuo Is there an ETA for GA? Thanks!

@kieronlanning Should be sometime in February.

Will this issue be fixed sometime by February? Is my understanding right ?
Right now we have upgraded to Aps.NetCore 2.2 and we are experiencing this issue, so do you think we need to downgrade to 2.1.7 ?

Correct, the fix will be released in February. Downgrading to 2.1 is also possible to work around the issue.

Looking forward to this fix!

We have a solution that uses X-Correlation-ID and we need to set the TraceIdentifier badly.
Hope this gets fixed on 2.2.2.

@FernandoNunes it's coming in a week or so :)

@muratg Thats great news, however we do are in a migration process and we did found a workaround after looking at how the "issued" HttpContextAcessor is implemeted, so we inject the HttpContextAcessor when we affect the TraceIdentifier and imediatly reset it so that it is available for the rest of the HttpRequest execution. We don't see any side-effects and after preliminary tests, seems a suitable workaround while we don't have 2.2.2 ready.

public class CorrelationIdMiddleware
{
    private readonly RequestDelegate _next;
    private readonly IHttpContextAccessor _httpAccessor;

    public CorrelationIdMiddleware(RequestDelegate next, IHttpContextAccessor httpAccessor)
    {
        _next = next;
        _httpAccessor = httpAccessor;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        if (context.Request.Headers.ContainsKey("X-Correlation-ID"))
        {
            context.TraceIdentifier = context.Request.Headers["X-Correlation-ID"];
            // WORKAROUND: On ASP.NET Core 2.2.1 we need to re-store in AsyncLocal the new TraceId, HttpContext Pair
            _httpAccessor.HttpContext = context;
        }

        // Call the next delegate/middleware in the pipeline
        await _next(context);
    }
}

Hope this helps.

Best Regards,
Fernando Nunes

_httpAccessor.HttpContext = context;

Thank you @FernandoNunes!
Faced the same issue in my current project.
Your shared solution was very useful. It's simple and works.
Moreover, it was provided at a very actual time.

@muratg not sure if you guys are planning to backport the fix further, but just a heads up: this exact same issue is present in v2.1.X as well. All the documentation on the bug I've seen assumes it started in 2.2.0 (e.g., one of the packages we're using: https://github.com/stevejgordon/CorrelationId#known-issue-with-aspnet-core-220) but we've confirmed the same behavior in several versions:

  • 2.1.0 : OK
  • 2.1.1, 2.1.2 : Untested
  • 2.1.3 : Bug present
  • 2.1.4, 2.1.6 : Untested, assuming is present
  • 2.1.7 (latest stable 2.1.X) : Bug present

Luckily @FernandoNunes's workaround works on those versions as well 👍

Folks, 2.2.2 (with the fix) was released yesterday!

@AdamSchueller thanks for the update. @davidfowl were you aware that this is broken in 2.1.x as well?

It’s not broken in 2.1 AFAIK. Would love a fully isolated repro. Even stranger is that the above claims it was broken in a 2.1 patch which seems unlikely (unless we backported it to a patch). Moving to mondo repo makes it hard to track these changes.

@davidfowl Apologies, you are correct: the bug is 2.2.0+ only. I was seeing the behavior in our mega-app after we upgraded from ASP.NET to ASP.NET Core 2.1 (ugh). When I tried to recreate in a isolated repo, I could not -- until I added the package references from the app.

The problem was one of the (many) references was silently bumping the version of Microsoft.AspNetCore.Http.dll (where this bug must live) to v2.2.0:

image

Removing the CookiePolicy reference shows that AspNetCore depends on that assembly as well, but now it's the expected version:

image

Updating to AspNetCore.App -- which lists an explicit dependency on Microsoft.AspNetCore.Http (>= 2.1.1 && < 2.2.0) -- gives an error that makes it obvious what's happening:

image

And of course, downgrading the CookiePolicy assembly fixes everything:

image

I did not mean to cry wolf :) Probably should have tested this outside of a project with 12,000+ files before making any claims. Microsoft.AspNetCore not explicitly declaring a dependency on the assembly with the bug made this trickier to find!

Here's the repo with the issue I was messing around with, just in case (though it's clearly not a bug in 2.1.X): https://github.com/AdamSchueller/HttpContextAccessor-Not-A-Bug

We are currently using 2.2.101 SDK with 2.2.0 runtime. I updated my machine to be 2.2.104 SDK and 2.2.2 Runtime. Things seem to work fine when I run my web service and hit it locally, however when I run Integration tests, which set up a TestServer, I run into this failure.

@parekhkb are you sure you're running 2.2.2 in your test setup?

@muratg Yes, the tests are running locally as well. I tried in CI as well and when I upgrade CI to 2.2.2 it fails there as well.

@parekhkb Can you provide a minimal repro of the problem?

@parekhkb please open a new issue with the details for your scenario.

@Tratcher once I get an isolated Repo I will. Thanks

@Tratcher I believe we have run into a similar issue with TestServer and I opened a new issue with a repro here: #7975

@brandonpollett @Tratcher I have a repo case using TestServer. From what I can tell, the HttpContext turns to null after I make an outbound HttpClient call from the app to a REST endpoint in the same app under test. I hope that makes sense. My unit test calls a Rest endpoint on the app and HttpContext is good, then I make an outbound call to another REST endpoint on the same app and when it comes back HttpContext is null.

public async Task<List<TokenExchangeResponse>> ProcessExchangeAsync(TokenExchangeRequest tokenExchangeRequest)
{
// _httpContextAssessor.HttpContext is good
      
            if (tokenExchangeRequest.Extras == null || tokenExchangeRequest.Extras.Count == 0)
            {
                throw new Exception($"{Name}: We require that extras be populated!");
            }
            List<ValidatedToken> validatedIdentityTokens = new List<ValidatedToken>();
            foreach (var item in tokenExchangeRequest.Tokens)
            {
// this following call makes an outbound call to an OAuth2 discovery endpoint to get certs
                var principal = await _tokenValidator.ValidateTokenAsync(new TokenDescriptor
                {
                    TokenScheme = item.TokenScheme,
                    Token = item.Token
                });
// principal is good
// _httpContextAssessor.HttpContext is NULL... ouch!.....
            }
}

@ghstahl this is a known issue with TestServer and is tracked here: #7975 (comment)

@brandonpollett Thank you so much for the workaround code at microsoft/fhir-server#418
@Tratcher looking forward to something going into TestServer. Setting up this workaround would have killed my day had it not been for @brandonpollett and @anurse

We are running SDK v2.2.105 and are still seeing this problem.
I believe my injection is wired up correctly

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc();
    services.AddHttpContextAccessor();
    services.AddTransient<IAppIdentity, AppIdentity>();
}
public class AppIdentity : IAppIdentity
{
    private readonly List<Claim> _claims;

    public AppIdentity(IHttpContextAccessor accessor)
    {
        var context = accessor.HttpContext;
        if (context == null)
        {
			throw new ArgumentNullException(nameof(context));
        }

        _claims = context.User?.Claims == null ? new List<Claim>() : context.User.Claims.ToList();
    }
            
    public int AppSourceKey => Convert.ToInt32(_claims.SingleOrDefault(w => w.Type == nameof(AppSourceKey))?.Value);
    public string AppSourceName => _claims.SingleOrDefault(w => w.Type == nameof(AppSourceName))?.Value ?? string.Empty;
}

Here's the SDK's reference on our project
image

What else should I check?

As a simple workaround you can also create your own implementation of IHttpContextAccessor:

namespace MyComponents
{
    public class HttpContextAccessor : IHttpContextAccessor
    {
        private static AsyncLocal<HttpContext> _httpContextCurrent = new AsyncLocal<HttpContext>();

        public HttpContext HttpContext
        {
            get => _httpContextCurrent.Value;
            set => _httpContextCurrent.Value = value;
        }
    }
}

and register that instead:

//services.AddHttpContextAccessor();
services.TryAddSingleton<IHttpContextAccessor, MyComponents.HttpContextAccessor>();

@dillinzser Thanks, but HttpContext is still null when I try to access it.

@Airn5475 can you open a new issue and include a complete runnable sample project? Since this issue is now closed we don't track it as closely and it seems like what you are seeing may be a new problem. We need a complete sample because AsyncLocals can be very dependent on how the code accessing them was launched (see below).

If @dillinzser 's workaround didn't work though, that strongly indicates that something in your app is dumping the AsyncLocal values. Do you have any code in your application that uses ThreadPool.UnsafeQueueUserWorkItem or calls ExecutionContext.SuppressFlow? Both of those would cause AsyncLocal values to be reset in the code that runs inside.

@anurse Thanks, I attempted to put together a complete running sample...and it worked fine. I tore apart my project and isolated the issue to Middleware and not doing my injection correctly.

I was passing in a dependency that depended on my aforementioned IAppIdentity into the constructor, when I should have been passing it to the Invoke method.

Posting this to perhaps help someone else.

public class ExceptionMiddleware
{
     private readonly RequestDelegate _next;

     public ExceptionMiddleware(RequestDelegate next /*, IAppLoggingRepository logger*/)
     {
         _next = next;
     }

     public async Task InvokeAsync(HttpContext httpContext, IAppLoggingRepository logger)
     {
         try
         {
             await _next(httpContext);
         }
         catch (Exception ex)
         {
             logger.LogException(ex);
             await HandleExceptionAsync(httpContext, ex);
         }
     }

     //...
}

Either should work

@davidfowl the constructor level injection doesn't work and I believe the explanation is here:
https://docs.microsoft.com/en-us/aspnet/core/fundamentals/middleware/write?view=aspnetcore-2.2#per-request-dependencies

Because middleware is constructed at app startup, not per-request, scoped lifetime services used by middleware constructors aren't shared with other dependency-injected types during each request. If you must share a scoped service between your middleware and other types, add these services to the Invoke method's signature.

In my example, the injected IAppLoggingRepository depends on IAppIdentity, which as a Transient per-request scope.