ASP.NET Core と Classic ASP の Cookie 共有について

n 年前に作った既存の Classic ASP (以下 ASP と記載) は動かしつつ、新しい機能は ASP.NET Core で実装していきたい :laughing: というケースを想定して Cookie を共有する検証してみました :raised_hand: 

# ASP での取得 / 設定

まず ASP (**Shift-JISのとき**)での Cookie 作成のコードを書いて実行してみます。
ASP では下記の `hoge1` のような[サブキー](https://msdn.microsoft.com/en-us/library/aa289495%28v=vs.71%29.aspx#vbtchaspnetcookies101anchor6)が使えます。
(ちなみにサブキーは [ASP.NET WebForm/MVC](http://miso-soup3.hateblo.jp/entry/2013/12/08/051521) でも使えます)

```html:SetCookie.asp
<%

Response.Write "Set Cookie <br/>"

Response.Cookies("test1") = ""
Response.Cookies("test1")("hoge1") = "こんにちは"
Response.Cookies("test1")("hoge2") = "こんばんは"
Response.Cookies("test1").domain = "localhost"
Response.Cookies("test1").path = "/"

Response.Write "Done"

%>
```

`test1` という Cookie ができました。

![image](https://qiita-image-store.s3.amazonaws.com/0/81135/965becc6-c962-d813-e6ec-3fd8d361b498.png)


```
hoge2=%82%B1%82%F1%82%CE%82%F1%82%CD&hoge1=%82%B1%82%F1%82%C9%82%BF%82%CD
```

次は、`GetCookie.asp` を作って Cookie を取得してみます。

```html:GetCookie.asp
<%

Response.Write "Get Cookie"
Response.Write "<br/>"
Response.Write Request.Cookies("test1")("hoge1")
Response.Write "<br/>"
Response.Write Request.Cookies("test1")("hoge2")
Response.Write "<br/>"
Response.Write "Done"

%>
```

ブラウザでアクセスするとちゃんと取得できました。

![image](https://qiita-image-store.s3.amazonaws.com/0/81135/b34a9478-0815-c6e2-c59b-2fe16366b09b.png)

# ASP.NET Core  での取得

ASP.NET Core のテンプレートアプリケーションで Cookie 操作をしてみます。
見ての通り、取得も設定も string の key/value を設定するだけのシンプルなインターフェースになっています。サブキーはデフォルトでは使えません。（**2016-12-22時点では**）

```csharp:HomeController.cs
public IActionResult About()
{
    // Set Cookie 
    Response.Cookies.Append("test2", "こんにちは", new CookieOptions() {Path = "/"});
    return View();
}

public IActionResult Contact()
{    
    // Get Cookie
    ViewData["test1"] = Request.Cookies["test1"];
    ViewData["test2"] = Request.Cookies["test2"];
    return View();
}
```

```html:Contact.cshtml
<h3>test1 = @ViewData["test1"]</h3>
<h3>test2 = @ViewData["test2"]</h3>
```

About ページにアクセスすると Cookie ができます。

![image](https://qiita-image-store.s3.amazonaws.com/0/81135/933c4c34-3bf7-e52a-f8f5-0323e63739c7.png)

```
%E3%81%93%E3%82%93%E3%81%AB%E3%81%A1%E3%81%AF
```

Contact ページにアクセスすると、ASP.NET Core で設定した `test2` はちゃんと表示されましたが、
ASP 側で設定した `test1` はいろいろやらないといけなそうです。

![image](https://qiita-image-store.s3.amazonaws.com/0/81135/5dff8530-5956-358a-1df4-ec93abc909b8.png)


`test1` の `hoge1` サブキーを取得してみます。
`&` と `=` で分解するだけですね。

```csharp
        public IActionResult Contact()
        {
            var test1 = Request.Cookies["test1"]
                                .Split('&')
                                .Select(k => k.Split('='))
                                .ToDictionary(s => s[0]?.Trim(), s => s[1]?.Trim());
            ViewData["test1"] = test1["hoge1"];

            ViewData["test2"] = Request.Cookies["test2"];
            return View();
        }
```

`hoge1` がとりあえず取り出せました。
が、Cookie に持っているのは `%82%B1%82%F1%82%C9%82%BF%82%CD` なのに、`%82%B1%82%F1%82ɂ%BF%82%CD` が表示されています。
Cookie を参照すると `Uri.UnescapeDataString` メソッドで自動的に Unescape されますが、
`test1` は Shift-JIS なので、UTFで Unescape されるとまずいですね。

(参考：[RequestCookieCollection のソース](https://github.com/aspnet/HttpAbstractions/blob/87cd79d6fc54bb4abf07c1e380cd7a9498a78612/src/Microsoft.AspNetCore.Http/Internal/RequestCookieCollection.cs#L83))

![image](https://qiita-image-store.s3.amazonaws.com/0/81135/44878f91-9724-eb03-2098-dfe019905f68.png)

そこで、Cookie Value を Unescape しないで Cookie を取得する `RawValueCookies` 拡張メソッドを作ってみます。

```csharp
        public IActionResult Contact()
        {
            var test1 = Request.RawValueCookies()["test1"]
                                .Split('&')
                                .Select(k => k.Split('='))
                                .ToDictionary(s => s[0]?.Trim(), s => s[1]?.Trim());
            ViewData["test1"] = test1["hoge1"];

            ViewData["test2"] = Request.Cookies["test2"];
            return View();
        }
```

```csharp:Extension,cs
        public static RequestCookieCollection RawValueCookies(this HttpRequest request)
        {
            StringValues current;
            if (!request.Headers.TryGetValue(HeaderNames.Cookie, out current))
            {
                current = string.Empty;
            }
            return Parse(current.ToArray());
        }

        public static readonly RequestCookieCollection Empty = new RequestCookieCollection();
        private static RequestCookieCollection Parse(IList<string> values)
        {
            if (values.Count == 0)
            {
                return Empty;
            }

            IList<CookieHeaderValue> cookies;
            if (CookieHeaderValue.TryParseList(values, out cookies))
            {
                if (cookies.Count == 0)
                {
                    return Empty;
                }

                var store = new Dictionary<string, string>(cookies.Count);
                for (var i = 0; i < cookies.Count; i++)
                {
                    var cookie = cookies[i];
                    var name = Uri.UnescapeDataString(cookie.Name);
                    //value を Uri.UnescapeDataString しない
                    var value = cookie.Value;
                    store[name] = value;
                }

                return new RequestCookieCollection(store);
            }
            return Empty;
        }
```

Unescape しないで取り出せました！

![image](https://qiita-image-store.s3.amazonaws.com/0/81135/e7f51050-504f-d3af-2779-04821cc0e1af.png)

あとは Shift-JIS から UTF-8 に変換すると・・・


```csharp
        public IActionResult Contact()
        {
            var test1 = Request.RawValueCookies()["test1"]
                                .Split('&')
                                .Select(k => k.Split('='))
                                .ToDictionary(s => s[0]?.Trim(), s => s[1]?.Trim());
            var bytes = Encoding.ASCII.GetBytes(test1["hoge1"]);
            var decode = System.Net.WebUtility.UrlDecodeToBytes(bytes, 0, bytes.Length);
            var convert = Encoding.Convert(Encoding.GetEncoding("Shift-JIS"), Encoding.UTF8, decode);
            ViewData["test1"] = Encoding.UTF8.GetString(convert);

            ViewData["test2"] = Request.Cookies["test2"];
            return View();
        }
```

こんにちは :smiley: 

![image](https://qiita-image-store.s3.amazonaws.com/0/81135/40841eea-75d4-d51d-4acc-193b919399c6.png)



# ASP.NET Core  での設定

ASP の Cookie を Raw Value で取得したということは設定時は Raw Value で戻してあげないといけないですね。[Append メソッド](https://github.com/aspnet/HttpAbstractions/blob/45696535048559188dce8b38a9fde82baa88893c/src/Microsoft.AspNetCore.Http/Internal/ResponseCookies.cs#L40)で value をエスケープしているので、ここだけ直した拡張メソッドを用意すればよさそうにみえます。
が、`SetCookieHeaderValue` 側のチェックに引っかかるので、ダミーを入れてヘッダーにセットする直前に置き換えてあげる必要があります :sweat_smile: 


```csharp
        public IActionResult Contact()
        {
            var test1 = Request.RawValueCookies()["test1"]
                                .Split('&')
                                .Select(k => k.Split('='))
                                .ToDictionary(s => s[0]?.Trim(), s => s[1]?.Trim());
            var bytes = Encoding.ASCII.GetBytes(test1["hoge1"]);
            var decode = System.Net.WebUtility.UrlDecodeToBytes(bytes, 0, bytes.Length);
            var convert = Encoding.Convert(Encoding.GetEncoding("Shift-JIS"), Encoding.UTF8, decode);
            ViewData["test1"] = Encoding.UTF8.GetString(convert);
            
            // set cookie
            Response.AppendRowValueCookie("test1", string.Join("&", test1.Select(s => s.Key + "=" + s.Value)), new CookieOptions() {Path = "/"});

            ViewData["test2"] = Request.Cookies["test2"];
            return View();
        }
```

```csharp:Extension.cs
       public static void AppendRowValueCookie(this HttpResponse response, string key, string value, CookieOptions options)
        {
            const string dummyValueString = "COOKIE_DUMMY_VALUE";
            if (options == null)
            {
                throw new ArgumentNullException(nameof(options));
            }

            // set dummy
            var setCookieHeaderValue = new SetCookieHeaderValue(Uri.EscapeDataString(key), dummyValueString)
            {
                Domain = options.Domain,
                Path = options.Path,
                Expires = options.Expires,
                Secure = options.Secure,
                HttpOnly = options.HttpOnly,
            };

            // replace dummy
            var cookieValue = setCookieHeaderValue.ToString().Replace(dummyValueString, value);
            response.Headers[HeaderNames.SetCookie] = StringValues.Concat(response.Headers[HeaderNames.SetCookie],  cookieValue);
        }
```




# その他の注意事項

.NET Core で Shift-JIS を扱うには `System.Text.Encoding.CodePages` が必要です。

```json:project.json
"dependencies": {
  "System.Text.Encoding.CodePages": "4.3.0"
}
```

```csharp:Startup.cs
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
```

# 謝辞

ハマってたら @shibayan がアドバイスくれたので感謝
![image](https://qiita-image-store.s3.amazonaws.com/0/12981/6a16ebdc-6527-095d-8bb9-4e75b2043cfa.png)