Neutral Scent

App developments & Gadgets

.NETのCookieContainerをシリアライズする

.NETのCookieContainerってなんでSerializeできないんですかね?
HttpWebRequestでcookieを横取りしたり、そもそもそんなことしたいと思うのが筋悪なのか? と、思いつつ幾数年...。
8.1のWindows Universal Appを作っていたら、なんとHttpCookieManagerなどと言うモノが新設されており、こりゃ楽になる! ...と思ったのもつかの間、今作ってるアプリはHTTP関連が全部PCLに入っていたという。
そんなわけで、今更感もありますが、あらためてCookieContainerとは何者かを見直して、この値をDataContractSerializerでシリアライズして保存可能なCookieContainrラッパークラスを実装してみました。
とりあえずDataContractSerializerを使っていますが、XmlSerializerやSerializable用にも書き換えられると思います。
今の所ちゃんと動いているようですが、何かおかしなところがあればツッコミお願いします。

[DataContract]
public class DataContractCookieContainer
{
    private DataContractCookieContainer()
    {
        this.CookieContainer = new CookieContainer();
    }

    public DataContractCookieContainer(string targetDomain, CookieContainer cookieContainer)
    {
        this.CookieContainer = cookieContainer;
        this.Domain = targetDomain;
    }

    // Order is very important factor in deserializing order
    [DataMember(Order=0)]
    public string Domain { get; set; }
    [IgnoreDataMember]
    public CookieContainer CookieContainer { get; set; }
    [DataMember(Order=1)]
    private List<DataContractCookie> Cookies
    {
        get
        {
            if (string.IsNullOrWhiteSpace(Domain) || !Domain.StartsWith("http"))
                throw new InvalidOperationException("Domain must be set Uri initializable URL start with http...");
            var cookielist = new List<DataContractCookie>();
            var collection = this.CookieContainer.GetCookies(new Uri(Domain));
            foreach (Cookie c in collection)
            {
                cookielist.Add(new DataContractCookie(c));
            }
            return cookielist;
        }
        set
        {
            this.CookieContainer = new CookieContainer();
            if (value != null && !string.IsNullOrWhiteSpace(Domain))
            {
                var collection = new CookieCollection();
                foreach (var cookie in value)
                {
                    collection.Add(cookie.ToCookie());
                }
                this.CookieContainer.Add(new Uri(Domain), collection);
            }
        }
    }

    [DataContract]
    private sealed class DataContractCookie
    {
        // Error message when you serialize a class by using the XMLSerializer class: "System.InvalidOperationException"
        // http://support.microsoft.com/kb/330592/en
        // http://support.microsoft.com/kb/330592/ja (Japanese)
        private DataContractCookie()
        {
        }

        public DataContractCookie(Cookie cookie)
        {
            Comment = cookie.Comment;
            CommentUri = cookie.CommentUri;
            Discard = cookie.Discard;
            Domain = cookie.Domain;
            Expired = cookie.Expired;
            Expires = cookie.Expires;
            HttpOnly = cookie.HttpOnly;
            Name = cookie.Name;
            Path = cookie.Path;
            Port = cookie.Port;
            Secure = cookie.Secure;
            Value = cookie.Value;
            Version = cookie.Version;
        }

        public Cookie ToCookie()
        {
            var cookie = new Cookie()
            {
                Name = Name,
                Value = Value,
                Path = Path,
                Domain = Domain,
                Comment = Comment,
                CommentUri = CommentUri,
                Discard = Discard,
                Expired = Expired,
                Expires = Expires,
                HttpOnly = HttpOnly,
                Port = Port,
                Secure = Secure,
                Version = Version,
            };
            return cookie;
        }

        [DataMember]
        public string Comment { get; set; }
        [DataMember]
        public string CommentUrl { get; set; }
        // Uri == null cause exception
        [IgnoreDataMember]
        public Uri CommentUri
        {
            get { return string.IsNullOrWhiteSpace(CommentUrl) ? null : new Uri(CommentUrl); }
            set { CommentUrl = (value == null) ? "" : value.ToString(); }
        }
        [DataMember]
        public bool Discard { get; set; }
        [DataMember]
        public string Domain { get; set; }
        [DataMember]
        public bool Expired { get; set; }
        [DataMember]
        public DateTime Expires { get; set; }
        [DataMember]
        public bool HttpOnly { get; set; }
        [DataMember]
        public string Name { get; set; }
        [DataMember]
        public string Path { get; set; }
        [DataMember]
        public string Port { get; set; }
        [DataMember]
        public bool Secure { get; set; }
        [DataMember]
        public string Value { get; set; }
        [DataMember]
        public int Version { get; set; }
    }
}

使い方は、↓こんな感じ。
DataContractCookieContainerはアプリで使用するcookieドメインのURL(http〜)を指定して初期化してください。クッキーを列挙するのにCookieContainer.GetCookies(uri)を使うしか無いもので。(緩募: Uriで全ドメインを指定する方法)

[DataContract]
public class SampleSettings
{
    private SampleSettings()
    {
    }

    public SampleSettings(string targetDomain, CookieContainer cookieContainer = null)
    {
        CookieData = new DataContractCookieContainer(targetDomain, cookieContainer ?? new CookieContainer());
    }

    public string ToXml()
    {
        try
        {
            var serializer = new DataContractSerializer(typeof(SampleSettings));
            using (var ms = new MemoryStream())
            {
                serializer.WriteObject(ms, this);
                return Encoding.UTF8.GetString(ms.ToArray(), 0, (int)ms.Length);
            }
        }
        catch (Exception ex)
        {
            Debug.Assert(false, "Something Error Handling");
            return string.Empty;
        }
    }

    public static SampleSettings ParseXml(string xml)
    {
        if (string.IsNullOrWhiteSpace(xml))
            return null;
        var bytes = Encoding.UTF8.GetBytes(xml);
        try
        {
            var serializer = new DataContractSerializer(typeof(SampleSettings));
            using (var stm = new MemoryStream(bytes))
            {
                return serializer.ReadObject(stm) as SampleSettings;
            }
        }
        catch (Exception ex)
        {
            Debug.Assert(false,"Something Error Handling");
            return new SampleSettings();
        }
    }

    [DataMember]
    public string Username { get; set; }
    [DataMember]
    public int UserID { get; set; }
    [DataMember]
    private DataContractCookieContainer CookieData { get; set; }
    [IgnoreDataMember]
    public CookieContainer Cookie 
    {
        get { return CookieData.CookieContainer; }
        set { CookieData.CookieContainer = value; } 
    }
}

順に書いて行ってわかったのは、どうもCookieContainerがシリアライズできないのは、最終的に各cookieが格納されているCookieクラスのCommentUriメンバーにnullが入ってるのが原因っぽいですね。そんなことかよ、という...。もしかしたらもっと深遠な理由があるのかもですが。