HttpContext.Current is null inside Identity Framew

2019-04-24 03:56发布

问题:

I am using ASP.NET MVC 5 and Identity Framework. When I call UserManager.UpdateAsync(...) my eventhandlers on ApplicationDbContext() SaveChanges will run. Here I am using HttpContext.Current for different purposes (logging and auditing) so I must get say current user. However the whole method runs in a worker thread, and here HttpContext.Current is null.

The biggest problem that the UserManager's "sync" methods are only wrappers around the async version, so the calls are serialized, but the methods (and eventhandlers) still run in a different worker thread.

Please note this issue has nothing to do with the async/await context. In the controller after the await (or calling the 'sync' version) I have back the correct HttpContext, even the controller's method is continuing in an other thread. That's fine.

So the problem is inside the async worker which will run in both the "sync" and async versions. I think I am understanding the phenomena (but I am not happy with the fake 'sync' method versions, real sync methods would not exhibit this issue.) I just does not know how to deal/workaround it.

[btw: Would not it be more natural to implement UserManager's operarations as simple pure sync versions, then wrap them by async multithreaded wrappers?. IF we continue this async fashion without thinking we will soon invent the async assignment operator. It costs me dozens of hours (just this issue), and costs worldwide zillion dollars, I am sure in many cases less return than its price.]

Bonus: We are talking about UserManager which's impact pretty marginal, but the same principles and issues can apply any out of the box library (black box for you) which authors do not implement sync versions and or do not care about the controller thread's context. What about EF, it is not so marginal... and what about DI containers instantiation infrastructure like "request scope" or "session scope". Surely they misbehave if resolving occurs in a thread with no HttpContext.Current. Recently I refreshed SendGrid NuGet, and (as a breaking change) Deliver() method gone, and now only DeliverAsync() is existing...

I would like to have a safe reliable way, how can I access the HttpContext inside this worker for logging and audit purposes.

Sample code, the controller 'sync' version:

[AcceptVerbs(HttpVerbs.Post)]
public virtual ActionResult Edit(ApplicationUser user)
{
    // validation etc
    // Update() seems to be only a poor wrapper around the async version, still uses a worker thread.
    var result = UserManager.Update(user);
    // Note: HttpContext is correct here so it is not an async/await problem

    // error handling, creating ActionResult etc.
}

Sample code, the controller async version:

[AcceptVerbs(HttpVerbs.Post)]
public virtual async Task<ActionResult> Edit(ApplicationUser user)
{
    // validation etc
    var result = await UserManager.UpdateAsync(user);
    // Note: HttpContext is correct here so it is not an async/await problem

    // error handling, creating ActionResult etc.
}

and the event handler where HttpContext is null:

public ApplicationDbContext() : base("DefaultConnection", false)
{
    InitializeAudit();
}

private void InitializeAudit()
{
    var octx = ((IObjectContextAdapter) this).ObjectContext;

    octx.SavingChanges +=
        (sender, args) =>
        {
            // HttpContext.Current is null here
        };
}

Any ideas?

回答1:

As you said, this occurs because of threading. The delegate runs in a different thread, making the HttpContext inaccessible.

You can move the variable outside of the delegate, making it a closure.

private void InitializeAudit()
{
    var octx = ((IObjectContextAdapter) this).ObjectContext;
    HttpContext context = HttpContext.Current;

    octx.SavingChanges +=
        (sender, args) =>
        {
            // context is not null
        };
}


回答2:

You are using asp.net identity through owin, so one instance of the dbcontext is created per request, and you can get this reference from anywhere in the request pipeline.

nb. this is handy but i think the dbcontext shouldn't be accessed outside the manager. In asp.net identity design, only the manager should be aware of the store. I believe the dbcontext is exposed because several asp.net identity middleware have a dependance on it.

But, it could help resolve you problem:

Allow your custom dbcontext handler to be set outside the class:

public EventHandler SavingChangesEventHandler
        {
            set
            {
                (((System.Data.Entity.Infrastructure.IObjectContextAdapter)this).ObjectContext).SavingChanges += value;
            } 
        }

Declare a custom ActionFilter class and register it, then override OnActionExecuting:

Filtering in ASP.NET MVC https://msdn.microsoft.com/en-us/library/gg416513(VS.98).aspx

public class CustomizeAppDbcontextFilter : ActionFilterAttribute
{
    public override void OnActionExecuting(HttpActionContext actionContext)
    {
        var dbcontext = HttpContext.GetOwinContext().Get<ApplicationDbContext>();    
        var currentuser = HttpContext.Current.User;

        dbcontext.SavingChangesEventHandler = (sender, args) =>
            {
                // use currentuser
            };  
    }    
}

you may need these using statements to be able to call the identity.owin extension methods:

using Microsoft.AspNet.Identity;

using Microsoft.AspNet.Identity.Owin;

You should be in the controller thread because OnActionExecuting is wrapping the controller action.

I did not test it, so it may need some polishing but the concept should work.