4

What's the easiest way to clone current request's HttpContext instance?

I'm developing an app in Asp.net MVC v1. I upgraded the regular PartialView capabilities to actually have sub-controllers that act very similar, but have their own context. When you use PartialViews you have to fill view data for the partial view in your main view's controller action. I created my own functionality that makes it possible to call controller actions from within a view. This way I get:

  • I don't have to provide sub-view's data in my main view's controller action
  • sub controller methods can manipulate data more encapsulated without any relation to other views/controllers

The problem is that each sub-controller request uses HttpContext. So when I set some HttpContext.Item in a sub-controller it actually populates HttpContext of the actual request.

That's why I want to clone HttpContext. I'm already using:

HttpContext subContext = new HttpContext(request, response);
// what happened to Session, User, Items etc. properties?

but this doesn't set anything else than request and response. But I would probably also need other properties and collections... Like Session, Items, User... etc.

Schalk Versteeg
  • 1,057
  • 13
  • 23
Robert Koritnik
  • 103,639
  • 52
  • 277
  • 404

5 Answers5

8

While the "Not Possible" answer is correct, there is an alternative that is much cleaner than writing values into the current context and then rewriting back to its original state. The solution is to make a new HttpContext object entirely that is based on the URL of your choosing.

// A new request/response is constructed to using a new URL.
// The new response is using a StreamWriter with null stream as a backing stream 
// which doesn't consume resources

using (var nullWriter = new StreamWriter(Stream.Null))
{
    var newRequestUri = new Uri("http://www.somewhere.com/some-resource/");
    var newRequest = new HttpRequest("", newRequestUri.ToString(), newRequestUri.Query);

    var newResponse = new HttpResponse(nullWriter);
    var newContext = new HttpContextWrapper(new HttpContext(newRequest, newResponse));

    // Work with the new context here before it is disposed...
} 

Reference: https://github.com/maartenba/MvcSiteMapProvider/issues/278#issuecomment-34905271

NightOwl888
  • 55,572
  • 24
  • 139
  • 212
2

Not possible

I guess an actual deep cloning is not possible because of server session state. Cloning would also have to clone this value, which is web server specific internal resource that is intrinsically static and can not be cloned. In this case a web server would have multiple Session objects for instance.

Workaround
Anyway. The workaround was to set additional context values before instantiating sub-controller processing. After processing is finished I reverted values back to original. So I actually had context as it was before.

Robert Koritnik
  • 103,639
  • 52
  • 277
  • 404
2

For ASP.Net Core/.Net 5 the following will work (based on the ASP.Net Core source code for SignalR, if you need more features just add them).

public static HttpContext Clone(this HttpContext httpContext, bool copyBody)
{
     var existingRequestFeature = httpContext.Features.Get<IHttpRequestFeature>();

     var requestHeaders = new Dictionary<string, StringValues>(existingRequestFeature.Headers.Count, StringComparer.OrdinalIgnoreCase);
     foreach (var header in existingRequestFeature.Headers)
     {
         requestHeaders[header.Key] = header.Value;
     }

     var requestFeature = new HttpRequestFeature
     {
         Protocol = existingRequestFeature.Protocol,
         Method = existingRequestFeature.Method,
         Scheme = existingRequestFeature.Scheme,
         Path = existingRequestFeature.Path,
         PathBase = existingRequestFeature.PathBase,
         QueryString = existingRequestFeature.QueryString,
         RawTarget = existingRequestFeature.RawTarget,
         Headers = new HeaderDictionary(requestHeaders),
     };

     if(copyBody)
     {
          // We need to buffer first, otherwise the body won't be copied
          // Won't work if the body stream was accessed already without calling EnableBuffering() first or without leaveOpen
          httpContext.Request.EnableBuffering();
          httpContext.Request.Body.Seek(0, SeekOrigin.Begin);
          requestFeature.Body = existingRequestFeature.Body;
     }

     var features = new FeatureCollection();
     features.Set<IHttpRequestFeature>(requestFeature);
        // Unless we need the response we can ignore it...
     features.Set<IHttpResponseFeature>(new HttpResponseFeature());
     features.Set<IHttpResponseBodyFeature>(new StreamResponseBodyFeature(Stream.Null));
     
     var newContext = new DefaultHttpContext(features);

     if (copyBody)
     {
         // Rewind for any future use...
         httpContext.Request.Body.Seek(0, SeekOrigin.Begin);
     }

        // Can happen if the body was not copied
     if(httpContext.Request.HasFormContentType && httpContext.Request.Form.Count != newContext.Request.Form.Count)
     {
         newContext.Request.Form = new Microsoft.AspNetCore.Http.FormCollection(httpContext.Request.Form.ToDictionary(f => f.Key, f => f.Value));
     }

     return newContext;            
}
Community
  • 1
  • 1
yoel halb
  • 12,188
  • 3
  • 57
  • 52
  • There's something missing in this code. "newContext" is never defined nor initialized but is used inside your last if-statement. Where does that come from? – fix Jul 15 '21 at 12:19
  • @fix: The variable few lines above `var context` should likely be called `var newContext` or change the `newCOntext used in the `if` statement to `context`. Of course I'm just speculating that that's the case but am pretty sure I'm right. – Robert Koritnik Jul 27 '21 at 14:29
0

The ASP.NET MVC framework intentionally makes dependencies to abstract classes with all members virtual. That simply says - extensibility.

Controllers depend on HttpContextBase, not HttpContext. Perhaps you can make your sub-controllers depend on HttpContextBase too so you can wrap it. Just my 2 cents.

Canton
  • 807
  • 4
  • 7
  • Actually I do create a HttpContextBase (HttpContextWrapper actually that is a child class of HttpContextBase) but it still doesn't solve my problem. This base just wraps original context, doesn't it? I guess that's the idea with this class. – Robert Koritnik Sep 23 '09 at 16:31
-2

I've used

<% Html.RenderAction("Action", "Controller"); %>

to great effect, allowing me to create completely isolated/escapsulated actions without resorting to complex code. This would seem to offer the same functionality without the same complexity.

The rendered views are standard partial views and the controller actions just like any other.

Lazarus
  • 41,906
  • 4
  • 43
  • 54
  • I have my my own implementation of this capability because the one you're talking about doesn't work as expected when various Http verbs are used etc. If you make a POST all sub actions will also see it as a post even though they shouldn't be. And other particularities (with validation etc)... So my advanced version upgrades this system and for it I need to clone HttpContext to make it work as really independant request. – Robert Koritnik Sep 23 '09 at 16:15