When I started working on this question, I thought it would be easy. After half a day of researches (so cool that I'm on vacation), I could finally share the results.
HttpContext.Response.Headers
has type of IHeaderDictionary
. By default, in ASP.NET Core application on Kestrel, FrameResponseHeaders implementation is used. The main logic resides in FrameHeaders
base class. This headers dictionary is highly optimized for seting / getting of frequently used standard http headers. Here is a code snippet that handles setting cookie (AddValueFast
method):
if ("Set-Cookie".Equals(key, StringComparison.OrdinalIgnoreCase))
{
if ((_bits & 67108864L) == 0)
{
_bits |= 67108864L;
_headers._SetCookie = value;
return true;
}
return false;
}
As far as StringComparison.OrdinalIgnoreCase
is used for key comparison, you can't set another cookie header that differs only by the case. This makes sense because HTTP headers are case-insensitive.
But let's try to overcome it.
The obvious solution here is to replace implementation of IHeaderDictionary
with the case-sensitive one. ASP.NET Core contains a lot of seams and extensibility points for this, starting from IHttpResponseFeature that contains setable Headers
property and ending with the possibility to replace implementation of HttpContext
.
Unfortunately, all those replacements will not do the trick when running on Kestrel. If you check source code of Frame class that is responsible for writing HTTP response headers, you will see that it creates instance of FrameResponseHeaders
by itself and does not respect any other instances set through IHttpResponseFeature
or HttpContext.Response.Headers
:
protected FrameResponseHeaders FrameResponseHeaders { get; } = new FrameResponseHeaders();
So we should return back to FrameResponseHeaders
and its base FrameHeaders
classes and try to adjust their behavior.
FrameResponseHeaders
class uses fast setting of known headers (see AddValueFast
above) but stores all other unknown headers in MaybeUnknown
field:
protected Dictionary<string, StringValues> MaybeUnknown;
which is initialized as:
MaybeUnknown = new Dictionary<string, StringValues>(StringComparer.OrdinalIgnoreCase);
We could try to bypass fast header setting and add them directly to the MaybeUnknown
dictionary. We should however replace the dictionary created with StringComparer.OrdinalIgnoreCase
comparer with the default implementation that is case-sensitive.
MaybeUnknown
is a protected field and again we can't make Kestrel to use our custom implementation for holding class. That's why we are forced to set this field through reflection.
I've put all this dirty code into extension class over FrameHeaders
:
public static class FrameHeadersExtensions
{
public static void MakeCaseInsensitive(this FrameHeaders target)
{
var fieldInfo = GetDictionaryField(target.GetType());
fieldInfo.SetValue(target, new Dictionary<string, StringValues>());
}
public static void AddCaseInsensitiveHeader(this FrameHeaders target, string key, string value)
{
var fieldInfo = GetDictionaryField(target.GetType());
var values = (Dictionary<string, StringValues>)fieldInfo.GetValue(target);
values.Add(key, value);
}
private static FieldInfo GetDictionaryField(Type headersType)
{
var fieldInfo = headersType.GetField("MaybeUnknown", BindingFlags.Instance | BindingFlags.NonPublic);
if (fieldInfo == null)
{
throw new InvalidOperationException("Failed to get field info");
}
return fieldInfo;
}
}
MakeCaseInsensitive
replaces MaybeUnknown
with case-sensitive dictionary.
AddCaseInsensitiveHeader
adds header directly to MaybeUnknown
dictionary bypassing fast header setting.
Remaining part is only to call these methods in appropriate places in the controller:
[Route("api/[controller]")]
public class TestController : Controller
{
[NonAction]
public override void OnActionExecuting(ActionExecutingContext context)
{
var responseHeaders = (FrameResponseHeaders)HttpContext.Response.Headers;
responseHeaders.MakeCaseInsensitive();
}
// GET api/values
[HttpGet]
public string Get()
{
var responseHeaders = (FrameResponseHeaders)HttpContext.Response.Headers;
responseHeaders.AddCaseInsensitiveHeader("Set-Cookie", "Cookies1");
responseHeaders.AddCaseInsensitiveHeader("SET-COOKIE", "Cookies2");
return "Hello";
}
}
Here is result headers set:

Described solution is a very dirty hack. It will work only with Kestrel and things could change with future releases. Everything would be much easier and cleaner if Kestrel fully supports ASP.NET seams. But if you don't have any other choices for this moment, I hope this will help you.