Implements System.Net.Http MultipartContent
authorMarek Safar <marek.safar@gmail.com>
Fri, 20 Apr 2012 08:17:49 +0000 (09:17 +0100)
committerMarek Safar <marek.safar@gmail.com>
Fri, 20 Apr 2012 15:12:41 +0000 (16:12 +0100)
14 files changed:
mcs/class/System.Net.Http/System.Net.Http.Headers/HttpContentHeaders.cs
mcs/class/System.Net.Http/System.Net.Http.Headers/HttpRequestHeaders.cs
mcs/class/System.Net.Http/System.Net.Http.Headers/HttpResponseHeaders.cs
mcs/class/System.Net.Http/System.Net.Http.Headers/Parser.cs
mcs/class/System.Net.Http/System.Net.Http.dll.sources
mcs/class/System.Net.Http/System.Net.Http/ByteArrayContent.cs
mcs/class/System.Net.Http/System.Net.Http/HttpClient.cs
mcs/class/System.Net.Http/System.Net.Http/HttpContent.cs
mcs/class/System.Net.Http/System.Net.Http/MultipartContent.cs [new file with mode: 0644]
mcs/class/System.Net.Http/System.Net.Http/MultipartFormDataContent.cs [new file with mode: 0644]
mcs/class/System.Net.Http/System.Net.Http/StreamContent.cs
mcs/class/System.Net.Http/System.Net.Http_test.dll.sources
mcs/class/System.Net.Http/Test/System.Net.Http/MultipartContentTest.cs [new file with mode: 0644]
mcs/class/System.Net.Http/Test/System.Net.Http/MultipartFormDataContentTest.cs [new file with mode: 0644]

index 0b66f3444c483fd623aab4005747cb32d4947d8a..57bda06b90c3cf431083d388615753634f1a3866 100644 (file)
@@ -125,7 +125,7 @@ namespace System.Net.Http.Headers
                                return GetValue<DateTimeOffset?> ("Expires");
                        }
                        set {
-                               AddOrRemove ("Expires", value);
+                               AddOrRemove ("Expires", value, Parser.DateTime.ToString);
                        }
                }
 
@@ -134,7 +134,7 @@ namespace System.Net.Http.Headers
                                return GetValue<DateTimeOffset?> ("Last-Modified");
                        }
                        set {
-                               AddOrRemove ("Last-Modified", value);
+                               AddOrRemove ("Last-Modified", value, Parser.DateTime.ToString);
                        }
                }
        }
index db94aaa0b0e208b59f0fb8cc97054654c3917c7e..8f512c6cd33eda0f68afdd8058794cc2be512db8 100644 (file)
@@ -117,7 +117,7 @@ namespace System.Net.Http.Headers
                                return GetValue<DateTimeOffset?> ("Date");
                        }
                        set {
-                               AddOrRemove ("Date", value);
+                               AddOrRemove ("Date", value, Parser.DateTime.ToString);
                        }
                }
 
@@ -180,7 +180,7 @@ namespace System.Net.Http.Headers
                                return GetValue<DateTimeOffset?> ("If-Modified-Since");
                        }
                        set {
-                               AddOrRemove ("If-Modified-Since", value);
+                               AddOrRemove ("If-Modified-Since", value, Parser.DateTime.ToString);
                        }
                }
 
@@ -204,7 +204,7 @@ namespace System.Net.Http.Headers
                                return GetValue<DateTimeOffset?> ("If-Unmodified-Since");
                        }
                        set {
-                               AddOrRemove ("If-Unmodified-Since", value);
+                               AddOrRemove ("If-Unmodified-Since", value, Parser.DateTime.ToString);
                        }
                }
 
index 2e60df36e4b0fcc5858f17ad2c5b609d4d0f2059..388f274e4eee240f683e59e63ba054168b5fb4dc 100644 (file)
@@ -91,7 +91,7 @@ namespace System.Net.Http.Headers
                                return GetValue<DateTimeOffset?> ("Date");
                        }
                        set {
-                               AddOrRemove ("Date", value);
+                               AddOrRemove ("Date", value, Parser.DateTime.ToString);
                        }
                }
 
index 114b0679894269431148eff245162bcd51c3b99b..840ffbe3567f34dfcd9823a1eb049297ae7a8a22 100644 (file)
@@ -93,6 +93,8 @@ namespace System.Net.Http.Headers
 
                public static class DateTime
                {
+                       public new static readonly Func<object, string> ToString = l => ((DateTimeOffset) l).ToString ("r", CultureInfo.InvariantCulture);
+                       
                        public static bool TryParse (string input, out DateTimeOffset result)
                        {
                                return Lexer.TryGetDateValue (input, out result);
index 7d522af878cd22ff599b37ba545981734dbb9d4e..01f340fe09692c62cf38f982a24c221ad188e1da 100644 (file)
@@ -14,6 +14,8 @@ System.Net.Http/HttpRequestException.cs
 System.Net.Http/HttpRequestMessage.cs
 System.Net.Http/HttpResponseMessage.cs
 System.Net.Http/MessageProcessingHandler.cs
+System.Net.Http/MultipartContent.cs
+System.Net.Http/MultipartFormDataContent.cs
 System.Net.Http/StreamContent.cs
 System.Net.Http/StringContent.cs
 System.Net.Http.Headers/AuthenticationHeaderValue.cs
index 46a92e38cd2279340ca717f686ef2b3fb83e261c..ab551ee2735eda53f85a205ac3cd72534495e952 100644 (file)
@@ -63,7 +63,7 @@ namespace System.Net.Http
                        return Task.FromResult<Stream> (new MemoryStream (content, offset, count));
                }
 
-               protected override Task SerializeToStreamAsync (Stream stream, TransportContext context)
+               protected internal override Task SerializeToStreamAsync (Stream stream, TransportContext context)
                {
                        return stream.WriteAsync (content, offset, count);
                }
index c2d7a5a29f6a8813f6feb6232ce50c19a5380ad1..2f4fe588d3fe7e64661687b9023af458e5d05199 100644 (file)
@@ -280,37 +280,37 @@ namespace System.Net.Http
                public async Task<byte[]> GetByteArrayAsync (string requestUri)
                {
                        var resp = await GetAsync (requestUri, HttpCompletionOption.ResponseContentRead).ConfigureAwait (false);
-                       return await resp.Content.ReadAsByteArrayAsync ();
+                       return await resp.Content.ReadAsByteArrayAsync ().ConfigureAwait (false);
                }
 
                public async Task<byte[]> GetByteArrayAsync (Uri requestUri)
                {
                        var resp = await GetAsync (requestUri, HttpCompletionOption.ResponseContentRead).ConfigureAwait (false);
-                       return await resp.Content.ReadAsByteArrayAsync ();
+                       return await resp.Content.ReadAsByteArrayAsync ().ConfigureAwait (false);
                }
 
                public async Task<Stream> GetStreamAsync (string requestUri)
                {
                        var resp = await GetAsync (requestUri, HttpCompletionOption.ResponseContentRead).ConfigureAwait (false);
-                       return await resp.Content.ReadAsStreamAsync ();
+                       return await resp.Content.ReadAsStreamAsync ().ConfigureAwait (false);
                }
 
                public async Task<Stream> GetStreamAsync (Uri requestUri)
                {
                        var resp = await GetAsync (requestUri, HttpCompletionOption.ResponseContentRead).ConfigureAwait (false);
-                       return await resp.Content.ReadAsStreamAsync ();
+                       return await resp.Content.ReadAsStreamAsync ().ConfigureAwait (false);
                }
 
                public async Task<string> GetStringAsync (string requestUri)
                {
                        var resp = await GetAsync (requestUri, HttpCompletionOption.ResponseContentRead).ConfigureAwait (false);
-                       return await resp.Content.ReadAsStringAsync ();
+                       return await resp.Content.ReadAsStringAsync ().ConfigureAwait (false);
                }
 
                public async Task<string> GetStringAsync (Uri requestUri)
                {
                        var resp = await GetAsync (requestUri, HttpCompletionOption.ResponseContentRead).ConfigureAwait (false);
-                       return await resp.Content.ReadAsStringAsync ();
+                       return await resp.Content.ReadAsStringAsync ().ConfigureAwait (false);
                }
        }
 }
index a150582c9ee36d8bd43143a7399251d18b0447cb..247e9c40454b3a2ae42ad9df4240b23dc0f69668 100644 (file)
@@ -61,7 +61,7 @@ namespace System.Net.Http
 
                protected async virtual Task<Stream> CreateContentReadStreamAsync ()
                {
-                       await LoadIntoBufferAsync ();
+                       await LoadIntoBufferAsync ().ConfigureAwait (false);
                        return buffer;
                }
 
@@ -117,7 +117,7 @@ namespace System.Net.Http
 
                public async Task<string> ReadAsStringAsync ()
                {
-                       await LoadIntoBufferAsync ();
+                       await LoadIntoBufferAsync ().ConfigureAwait (false);
                        if (buffer.Length == 0)
                                return string.Empty;
 
@@ -131,7 +131,7 @@ namespace System.Net.Http
                        return encoding.GetString (buffer.GetBuffer (), 0, (int) buffer.Length);
                }
 
-               protected abstract Task SerializeToStreamAsync (Stream stream, TransportContext context);
+               protected internal abstract Task SerializeToStreamAsync (Stream stream, TransportContext context);
                protected internal abstract bool TryComputeLength (out long length);
        }
 }
diff --git a/mcs/class/System.Net.Http/System.Net.Http/MultipartContent.cs b/mcs/class/System.Net.Http/System.Net.Http/MultipartContent.cs
new file mode 100644 (file)
index 0000000..a607167
--- /dev/null
@@ -0,0 +1,220 @@
+//
+// MultipartContent.cs
+//
+// Authors:
+//     Marek Safar  <marek.safar@gmail.com>
+//
+// Copyright (C) 2012 Xamarin Inc (http://www.xamarin.com)
+//
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+//
+
+using System.IO;
+using System.Threading.Tasks;
+using System.Collections;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Net.Http.Headers;
+using System.Linq;
+using System.Text;
+
+namespace System.Net.Http
+{
+       public class MultipartContent : HttpContent, IEnumerable<HttpContent>
+       {
+               List<HttpContent> nested_content;
+               readonly string boundary;
+
+               public MultipartContent ()
+                       : this ("mixed")
+               {
+               }
+
+               public MultipartContent (string subtype)
+                       : this (subtype, Guid.NewGuid ().ToString ("D", CultureInfo.InvariantCulture))
+               {
+               }
+
+               public MultipartContent (string subtype, string boundary)
+               {
+                       if (string.IsNullOrWhiteSpace (subtype))
+                               throw new ArgumentException ("boundary");
+
+                       //
+                       // The only mandatory parameter for the multipart Content-Type is the boundary parameter, which consists
+                       // of 1 to 70 characters from a set of characters known to be very robust through email gateways,
+                       // and NOT ending with white space
+                       //
+                       if (string.IsNullOrWhiteSpace (boundary))
+                               throw new ArgumentException ("boundary");
+
+                       if (boundary.Length > 70)
+                               throw new ArgumentOutOfRangeException ("boundary");
+
+                       if (boundary.Last () == ' ' || !IsValidRFC2049 (boundary))
+                               throw new ArgumentException ("boundary");
+
+                       this.boundary = boundary;
+                       this.nested_content = new List<HttpContent> (2);
+
+                       Headers.ContentType = new MediaTypeHeaderValue ("multipart/" + subtype) {
+                               Parameters = { new NameValueHeaderValue ("boundary", "\"" + boundary + "\"") }
+                       };
+               }
+
+               static bool IsValidRFC2049 (string s)
+               {
+                       foreach (char c in s) {
+                               if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9'))
+                                       continue;
+
+                               switch (c) {
+                               case '\'': case '(': case ')': case '+': case ',':
+                               case '-': case '.': case '/': case ':': case '=':
+                               case '?':
+                                       continue;
+                               }
+
+                               return false;
+                       }
+
+                       return true;
+               }
+
+               public virtual void Add (HttpContent content)
+               {
+                       if (content == null)
+                               throw new ArgumentNullException ("content");
+
+                       if (nested_content == null)
+                               nested_content = new List<HttpContent> ();
+
+                       nested_content.Add (content);
+               }
+
+               protected override void Dispose (bool disposing)
+               {
+                       if (disposing) {
+                               foreach (var item in nested_content) {
+                                       item.Dispose ();
+                               }
+                               
+                               nested_content = null;
+                       }
+                       
+                       base.Dispose (disposing);
+               }
+
+               protected internal override async Task SerializeToStreamAsync (Stream stream, TransportContext context)
+               {
+                       // RFC 2046
+                       //
+                       // The Content-Type field for multipart entities requires one parameter,
+                       // "boundary". The boundary delimiter line is then defined as a line
+                       // consisting entirely of two hyphen characters ("-", decimal value 45)
+                       // followed by the boundary parameter value from the Content-Type header
+                       // field, optional linear whitespace, and a terminating CRLF.
+                       //
+
+                       byte[] buffer;
+                       var sb = new StringBuilder ();
+                       sb.Append ('-').Append ('-');
+                       sb.Append (boundary);
+                       sb.Append ('\r').Append ('\n');
+
+                       for (int i = 0; i < nested_content.Count; i++) {
+                               var c = nested_content [i];
+                                       
+                               foreach (var h in c.Headers) {
+                                       sb.Append (h.Key);
+                                       sb.Append (':').Append (' ');
+                                       foreach (var v in h.Value) {
+                                               sb.Append (v);
+                                       }
+                                       sb.Append ('\r').Append ('\n');
+                               }
+                               sb.Append ('\r').Append ('\n');
+                                       
+                               buffer = Encoding.ASCII.GetBytes (sb.ToString ());
+                               sb.Clear ();
+                               await stream.WriteAsync (buffer, 0, buffer.Length).ConfigureAwait (false);
+
+                               await c.SerializeToStreamAsync (stream, context).ConfigureAwait (false);
+                                       
+                               if (i != nested_content.Count - 1) {
+                                       sb.Append ('\r').Append ('\n');
+                                       sb.Append ('-').Append ('-');
+                                       sb.Append (boundary);
+                                       sb.Append ('\r').Append ('\n');
+                               }
+                       }
+                       
+                       sb.Append ('\r').Append ('\n');
+                       sb.Append ('-').Append ('-');
+                       sb.Append (boundary);
+                       sb.Append ('-').Append ('-');
+                       sb.Append ('\r').Append ('\n');
+
+                       buffer = Encoding.ASCII.GetBytes (sb.ToString ());
+                       await stream.WriteAsync (buffer, 0, buffer.Length).ConfigureAwait (false);
+               }
+               
+               protected internal override bool TryComputeLength (out long length)
+               {
+                       length = 12 + 2 * boundary.Length;
+                       
+                       for (int i = 0; i < nested_content.Count; i++) {
+                               var c = nested_content [i];
+                               foreach (var h in c.Headers) {
+                                       length += h.Key.Length;
+                                       length += 4;
+                                               
+                                       foreach (var v in h.Value) {
+                                               length += v.Length;
+                                       }
+                               }
+                                       
+                               long l;
+                               if (!c.TryComputeLength (out l))
+                                       return false;
+
+                               length += 2;
+                               length += l;
+                                       
+                               if (i != nested_content.Count - 1) {
+                                       length += 6;
+                                       length += boundary.Length;
+                               }
+                       }
+
+                       return true;
+               }
+
+               public IEnumerator<HttpContent> GetEnumerator ()
+               {
+                       return nested_content.GetEnumerator ();
+               }
+
+               IEnumerator IEnumerable.GetEnumerator ()
+               {
+                       return nested_content.GetEnumerator ();
+               }
+       }
+}
diff --git a/mcs/class/System.Net.Http/System.Net.Http/MultipartFormDataContent.cs b/mcs/class/System.Net.Http/System.Net.Http/MultipartFormDataContent.cs
new file mode 100644 (file)
index 0000000..c700ed4
--- /dev/null
@@ -0,0 +1,87 @@
+//
+// MultipartFormDataContent.cs
+//
+// Authors:
+//     Marek Safar  <marek.safar@gmail.com>
+//
+// Copyright (C) 2012 Xamarin Inc (http://www.xamarin.com)
+//
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+//
+
+using System.Net.Http.Headers;
+
+namespace System.Net.Http
+{
+       public class MultipartFormDataContent : MultipartContent
+       {
+               public MultipartFormDataContent ()
+                       : base ("form-data")
+               {
+               }
+
+               public MultipartFormDataContent (string boundary)
+                       : base ("form-data", boundary)
+               {
+               }
+               
+               public override void Add (HttpContent content)
+               {
+                       base.Add (content);
+                       AddContentDisposition (content, null, null);
+               }
+
+               public void Add (HttpContent content, string name)
+               {
+                       base.Add (content);
+                       
+                       if (string.IsNullOrWhiteSpace (name))
+                               throw new ArgumentException ("name");
+                       
+                       AddContentDisposition (content, name, null);
+               }
+               
+               public void Add (HttpContent content, string name, string fileName)
+               {
+                       base.Add (content);
+                       
+                       if (string.IsNullOrWhiteSpace (name))
+                               throw new ArgumentException ("name");
+                       
+                       if (string.IsNullOrWhiteSpace (fileName))
+                               throw new ArgumentException ("fileName");
+                       
+                       AddContentDisposition (content, name, fileName);
+               }
+               
+               void AddContentDisposition (HttpContent content, string name, string fileName)
+               {
+                       var headers = content.Headers;
+                       if (headers.ContentDisposition != null)
+                               return;
+                       
+                       headers.ContentDisposition = new ContentDispositionHeaderValue ("form-data") {
+                               Name = name,
+                               FileName = fileName,
+                               FileNameStar = fileName
+                       };
+               }
+       }
+}
index 6e3561a38bf744fa9d27c9a5d33ffa38e6d7b615..5bcb104336f4c45f9fff701463d57feca7fe87df 100644 (file)
@@ -67,7 +67,7 @@ namespace System.Net.Http
                        base.Dispose (disposing);
                }
 
-               protected override Task SerializeToStreamAsync (Stream stream, TransportContext context)
+               protected internal override Task SerializeToStreamAsync (Stream stream, TransportContext context)
                {
                        return content.CopyToAsync (stream, bufferSize);
                }
index ea1e0d882b51a7349d9b0fe58f360ba1cb3927a0..e5ce8f75050aa04794f336e16578f3531904236a 100644 (file)
@@ -5,6 +5,8 @@ System.Net.Http/HttpClientTest.cs
 System.Net.Http/HttpMethodTest.cs
 System.Net.Http/HttpRequestMessageTest.cs
 System.Net.Http/HttpResponseMessageTest.cs
+System.Net.Http/MultipartContentTest.cs
+System.Net.Http/MultipartFormDataContentTest.cs
 System.Net.Http/StreamContentTest.cs
 System.Net.Http/StringContentTest.cs
 System.Net.Http.Headers/AuthenticationHeaderValueTest.cs
diff --git a/mcs/class/System.Net.Http/Test/System.Net.Http/MultipartContentTest.cs b/mcs/class/System.Net.Http/Test/System.Net.Http/MultipartContentTest.cs
new file mode 100644 (file)
index 0000000..177df39
--- /dev/null
@@ -0,0 +1,165 @@
+//
+// MultipartContentTest.cs
+//
+// Authors:
+//     Marek Safar  <marek.safar@gmail.com>
+//
+// Copyright (C) 2012 Xamarin Inc (http://www.xamarin.com)
+//
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+//
+
+using System;
+using NUnit.Framework;
+using System.Net.Http;
+using System.IO;
+using System.Threading.Tasks;
+using System.Text;
+using System.Linq;
+
+namespace MonoTests.System.Net.Http
+{
+       [TestFixture]
+       public class MultipartContentTest
+       {
+               [Test]
+               public void Ctor_Invalid ()
+               {
+                       try {
+                               new MultipartContent (null);
+                               Assert.Fail ("#1");
+                       } catch (ArgumentException) {
+                       }
+
+                       try {
+                               new MultipartContent ("v", null);
+                               Assert.Fail ("#2");
+                       } catch (ArgumentException) {
+                       }
+
+                       try {
+                               new MultipartContent ("st", "[]");
+                               Assert.Fail ("#3");
+                       } catch (ArgumentException) {
+                       }
+
+                       try {
+                               new MultipartContent ("st", "1234567890123456789012345678901234567890123456789012345678901234567890X");
+                               Assert.Fail ("#4");
+                       } catch (ArgumentException) {
+                       }
+
+                       try {
+                               new MultipartContent ("st", "st ");
+                               Assert.Fail ("#5");
+                       } catch (ArgumentException) {
+                       }
+
+                       try {
+                               new MultipartContent ("st", "@");
+                               Assert.Fail ("#6");
+                       } catch (ArgumentException) {
+                       }
+               }
+
+               [Test]
+               public void Ctor ()
+               {
+                       using (var m = new MultipartContent ("a", "b")) {
+                               m.Headers.Add ("extra", "value");
+                               Assert.AreEqual ("multipart/a", m.Headers.ContentType.MediaType, "#1");
+                               Assert.AreEqual (14, m.Headers.ContentLength, "#2");
+                               Assert.AreEqual ("--b\r\n\r\n--b--\r\n", m.ReadAsStringAsync ().Result, "#3");
+                       }
+
+                       using (var m = new MultipartContent ()) {
+                               Assert.AreEqual ("multipart/mixed", m.Headers.ContentType.MediaType, "#11");
+                               Assert.AreEqual (84, m.Headers.ContentLength, "#12");
+                       }
+
+                       using (var m = new MultipartContent ("ggg")) {
+                               Assert.AreEqual ("multipart/ggg", m.Headers.ContentType.MediaType, "#21");
+                               Assert.AreEqual (84, m.Headers.ContentLength, "#22");
+                       }
+               }
+
+               [Test]
+               public void Add ()
+               {
+                       var m = new MultipartContent ("a", "b");
+
+                       var other = new MultipartContent ("2", "44");
+                       other.Headers.Expires = new DateTimeOffset (2020, 11, 30, 19, 55, 22, TimeSpan.Zero);
+                       m.Add (other);
+
+                       Assert.AreEqual ("multipart/a", m.Headers.ContentType.MediaType, "#1");
+                       Assert.AreEqual (114, m.Headers.ContentLength, "#2");
+                       Assert.AreEqual ("--b\r\nContent-Type: multipart/2; boundary=\"44\"\r\nExpires: Mon, 30 Nov 2020 19:55:22 GMT\r\n\r\n--44\r\n\r\n--44--\r\n\r\n--b--\r\n", m.ReadAsStringAsync ().Result, "#3");
+                       Assert.AreEqual (other, m.First (), "#4");
+               }
+
+               [Test]
+               public void Add_2 ()
+               {
+                       var m = new MultipartContent ("a", "X");
+
+                       var other = new MultipartContent ("2", "2a");
+                       m.Add (other);
+                       var other2 = new MultipartContent ("3", "3a");
+                       other2.Headers.Add ("9", "9n");
+                       m.Add (other2);
+
+                       Assert.AreEqual ("multipart/a", m.Headers.ContentType.MediaType, "#1");
+                       Assert.AreEqual (148, m.Headers.ContentLength, "#2");
+                       Assert.AreEqual ("--X\r\nContent-Type: multipart/2; boundary=\"2a\"\r\n\r\n--2a\r\n\r\n--2a--\r\n\r\n--X\r\nContent-Type: multipart/3; boundary=\"3a\"\r\n9: 9n\r\n\r\n--3a\r\n\r\n--3a--\r\n\r\n--X--\r\n",
+                               m.ReadAsStringAsync ().Result, "#3");
+                       Assert.AreEqual (other, m.First (), "#4");
+               }
+
+               [Test]
+               public void Add_Resursive ()
+               {
+                       var m = new MultipartContent ("1", "1a");
+
+                       var other = new MultipartContent ("2", "2a");
+                       m.Add (other);
+
+                       var other2 = new MultipartContent ("3", "3a");
+                       other.Add (other2);
+
+                       Assert.AreEqual ("multipart/1", m.Headers.ContentType.MediaType, "#1");
+                       Assert.AreEqual (136, m.Headers.ContentLength, "#2");
+                       Assert.AreEqual ("--1a\r\nContent-Type: multipart/2; boundary=\"2a\"\r\n\r\n--2a\r\nContent-Type: multipart/3; boundary=\"3a\"\r\n\r\n--3a\r\n\r\n--3a--\r\n\r\n--2a--\r\n\r\n--1a--\r\n",
+                               m.ReadAsStringAsync ().Result, "#3");
+                       Assert.AreEqual (other, m.First (), "#4");
+               }
+
+               [Test]
+               public void Add_Invalid ()
+               {
+                       var m = new MultipartContent ("a", "b");
+                       try {
+                               m.Add (null);
+                               Assert.Fail ("#1");
+                       } catch (ArgumentNullException) {
+                       }
+               }
+       }
+}
diff --git a/mcs/class/System.Net.Http/Test/System.Net.Http/MultipartFormDataContentTest.cs b/mcs/class/System.Net.Http/Test/System.Net.Http/MultipartFormDataContentTest.cs
new file mode 100644 (file)
index 0000000..2c9826d
--- /dev/null
@@ -0,0 +1,178 @@
+//
+// MultipartFormDataContentTest.cs
+//
+// Authors:
+//     Marek Safar  <marek.safar@gmail.com>
+//
+// Copyright (C) 2012 Xamarin Inc (http://www.xamarin.com)
+//
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+//
+
+using System;
+using NUnit.Framework;
+using System.Net.Http;
+using System.IO;
+using System.Threading.Tasks;
+using System.Text;
+using System.Linq;
+using System.Net.Http.Headers;
+
+namespace MonoTests.System.Net.Http
+{
+       [TestFixture]
+       public class MultipartFormDataContentTest
+       {
+               [Test]
+               public void Ctor_Invalid ()
+               {
+                       try {
+                               new MultipartFormDataContent (null);
+                               Assert.Fail ("#1");
+                       } catch (ArgumentException) {
+                       }
+
+                       try {
+                               new MultipartFormDataContent ("[]");
+                               Assert.Fail ("#2");
+                       } catch (ArgumentException) {
+                       }
+
+                       try {
+                               new MultipartFormDataContent ("1234567890123456789012345678901234567890123456789012345678901234567890X");
+                               Assert.Fail ("#3");
+                       } catch (ArgumentException) {
+                       }
+
+                       try {
+                               new MultipartFormDataContent ("st ");
+                               Assert.Fail ("#4");
+                       } catch (ArgumentException) {
+                       }
+
+                       try {
+                               new MultipartFormDataContent ("@");
+                               Assert.Fail ("#5");
+                       } catch (ArgumentException) {
+                       }
+               }
+
+               [Test]
+               public void Ctor ()
+               {
+                       using (var m = new MultipartFormDataContent ("b")) {
+                               m.Headers.Add ("extra", "value");
+                               Assert.AreEqual ("multipart/form-data", m.Headers.ContentType.MediaType, "#1");
+                               Assert.IsNull (m.Headers.ContentDisposition, "#2");
+                               Assert.AreEqual (14, m.Headers.ContentLength, "#3");
+                               Assert.AreEqual ("--b\r\n\r\n--b--\r\n", m.ReadAsStringAsync ().Result, "#4");
+                       }
+
+                       using (var m = new MultipartFormDataContent ()) {
+                               Assert.AreEqual ("multipart/form-data", m.Headers.ContentType.MediaType, "#11");
+                               Assert.AreEqual (84, m.Headers.ContentLength, "#12");
+                       }
+
+                       using (var m = new MultipartFormDataContent ("ggg")) {
+                               Assert.AreEqual ("multipart/form-data", m.Headers.ContentType.MediaType, "#21");
+                               Assert.AreEqual (18, m.Headers.ContentLength, "#22");
+                       }
+               }
+
+               [Test]
+               public void Add ()
+               {
+                       var m = new MultipartFormDataContent ("b");
+
+                       var other = new MultipartFormDataContent ("44");
+                       other.Headers.Expires = new DateTimeOffset (2020, 11, 30, 19, 55, 22, TimeSpan.Zero);
+                       m.Add (other);
+
+                       Assert.AreEqual ("multipart/form-data", m.Headers.ContentType.MediaType, "#1");
+                       Assert.AreEqual (154, m.Headers.ContentLength, "#2");
+                       Assert.AreEqual ("--b\r\nContent-Type: multipart/form-data; boundary=\"44\"\r\nExpires: Mon, 30 Nov 2020 19:55:22 GMT\r\nContent-Disposition: form-data\r\n\r\n--44\r\n\r\n--44--\r\n\r\n--b--\r\n", m.ReadAsStringAsync ().Result, "#3");
+                       Assert.AreEqual (other, m.First (), "#4");
+                       Assert.IsNull (m.Headers.ContentDisposition, "#5");
+                       Assert.AreEqual ("form-data", other.Headers.ContentDisposition.ToString (), "#6");
+               }
+
+               [Test]
+               public void Add_2 ()
+               {
+                       var m = new MultipartFormDataContent ("b");
+
+                       var other = new MultipartFormDataContent ("44");
+                       m.Add (other, "name", "fname");
+
+                       Assert.AreEqual ("multipart/form-data", m.Headers.ContentType.MediaType, "#1");
+                       Assert.AreEqual (165, m.Headers.ContentLength, "#2");
+                       Assert.AreEqual ("--b\r\nContent-Type: multipart/form-data; boundary=\"44\"\r\nContent-Disposition: form-data; name=name; filename=fname; filename*=utf-8''fname\r\n\r\n--44\r\n\r\n--44--\r\n\r\n--b--\r\n", m.ReadAsStringAsync ().Result, "#3");
+                       Assert.AreEqual (other, m.First (), "#4");
+                       Assert.IsNull (m.Headers.ContentDisposition, "#5");
+                       Assert.AreEqual ("form-data; name=name; filename=fname; filename*=utf-8''fname", other.Headers.ContentDisposition.ToString (), "#6");
+               }
+
+               [Test]
+               public void Add_3 ()
+               {
+                       var m = new MultipartFormDataContent ("b");
+
+                       var other = new MultipartFormDataContent ("44");
+                       other.Headers.ContentDisposition = new ContentDispositionHeaderValue ("dt");
+                       m.Add (other, "name", "fname");
+
+                       Assert.AreEqual ("multipart/form-data", m.Headers.ContentType.MediaType, "#1");
+                       Assert.AreEqual (107, m.Headers.ContentLength, "#2");
+                       Assert.AreEqual ("--b\r\nContent-Type: multipart/form-data; boundary=\"44\"\r\nContent-Disposition: dt\r\n\r\n--44\r\n\r\n--44--\r\n\r\n--b--\r\n", m.ReadAsStringAsync ().Result, "#3");
+                       Assert.AreEqual (other, m.First (), "#4");
+                       Assert.IsNull (m.Headers.ContentDisposition, "#5");
+                       Assert.AreEqual ("dt", other.Headers.ContentDisposition.ToString (), "#6");
+               }
+
+               [Test]
+               public void Add_Invalid ()
+               {
+                       var m = new MultipartFormDataContent ("a");
+                       try {
+                               m.Add (null);
+                               Assert.Fail ("#1");
+                       } catch (ArgumentNullException) {
+                       }
+
+                       try {
+                               m.Add (new MultipartFormDataContent ("44"), null);
+                               Assert.Fail ("#2");
+                       } catch (ArgumentException) {
+                       }
+
+                       try {
+                               m.Add (new MultipartFormDataContent ("44"), "s", null);
+                               Assert.Fail ("#3");
+                       } catch (ArgumentException) {
+                       }
+
+                       try {
+                               m.Add (new MultipartFormDataContent ("44"), "s", "   ");
+                               Assert.Fail ("#4");
+                       } catch (ArgumentException) {
+                       }
+               }
+       }
+}