Skip to content
This repository was archived by the owner on Nov 20, 2018. It is now read-only.

Commit 652d885

Browse files
committed
#177 Immutable HeaderValue objects.
1 parent 12b78a3 commit 652d885

File tree

8 files changed

+233
-36
lines changed

8 files changed

+233
-36
lines changed

src/Microsoft.Net.Http.Headers/HeaderUtilities.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,5 +228,13 @@ public static string RemoveQuotes(string input)
228228
}
229229
return input;
230230
}
231+
232+
internal static void ThrowIfReadOnly(bool isReadOnly)
233+
{
234+
if (isReadOnly)
235+
{
236+
throw new InvalidOperationException("The object cannot be modified because it is read-only.");
237+
}
238+
}
231239
}
232240
}

src/Microsoft.Net.Http.Headers/MediaTypeHeaderValue.cs

Lines changed: 52 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Collections.Generic;
66
using System.Diagnostics.Contracts;
77
using System.Globalization;
8+
using System.Linq;
89
using System.Text;
910

1011
namespace Microsoft.Net.Http.Headers
@@ -19,9 +20,10 @@ private static readonly HttpHeaderParser<MediaTypeHeaderValue> SingleValueParser
1920
private static readonly HttpHeaderParser<MediaTypeHeaderValue> MultipleValueParser
2021
= new GenericHeaderParser<MediaTypeHeaderValue>(true, GetMediaTypeLength);
2122

22-
// Use list instead of dictionary since we may have multiple parameters with the same name.
23-
private ICollection<NameValueHeaderValue> _parameters;
23+
// Use a collection instead of a dictionary since we may have multiple parameters with the same name.
24+
private ObjectCollection<NameValueHeaderValue> _parameters;
2425
private string _mediaType;
26+
private bool _isReadOnly;
2527

2628
private MediaTypeHeaderValue()
2729
{
@@ -48,6 +50,7 @@ public string Charset
4850
}
4951
set
5052
{
53+
HeaderUtilities.ThrowIfReadOnly(IsReadOnly);
5154
// We don't prevent a user from setting whitespace-only charsets. Like we can't prevent a user from
5255
// setting a non-existing charset.
5356
var charsetParameter = NameValueHeaderValue.Find(_parameters, CharsetString);
@@ -93,6 +96,7 @@ public Encoding Encoding
9396
}
9497
set
9598
{
99+
HeaderUtilities.ThrowIfReadOnly(IsReadOnly);
96100
if (value == null)
97101
{
98102
Charset = null;
@@ -112,6 +116,7 @@ public string Boundary
112116
}
113117
set
114118
{
119+
HeaderUtilities.ThrowIfReadOnly(IsReadOnly);
115120
var boundaryParameter = NameValueHeaderValue.Find(_parameters, BoundaryString);
116121
if (string.IsNullOrEmpty(value))
117122
{
@@ -141,7 +146,14 @@ public ICollection<NameValueHeaderValue> Parameters
141146
{
142147
if (_parameters == null)
143148
{
144-
_parameters = new ObjectCollection<NameValueHeaderValue>();
149+
if (IsReadOnly)
150+
{
151+
_parameters = ObjectCollection<NameValueHeaderValue>.EmptyReadOnlyCollection;
152+
}
153+
else
154+
{
155+
_parameters = new ObjectCollection<NameValueHeaderValue>();
156+
}
145157
}
146158
return _parameters;
147159
}
@@ -158,6 +170,7 @@ public string MediaType
158170
get { return _mediaType; }
159171
set
160172
{
173+
HeaderUtilities.ThrowIfReadOnly(IsReadOnly);
161174
CheckMediaTypeFormat(value, "value");
162175
_mediaType = value;
163176
}
@@ -201,6 +214,11 @@ public bool MatchesAllSubTypes
201214
}
202215
}
203216

217+
public bool IsReadOnly
218+
{
219+
get { return _isReadOnly; }
220+
}
221+
204222
public bool IsSubsetOf(MediaTypeHeaderValue otherMediaType)
205223
{
206224
if (otherMediaType == null)
@@ -253,19 +271,44 @@ public bool IsSubsetOf(MediaTypeHeaderValue otherMediaType)
253271
/// while avoiding the cost of revalidating the components.
254272
/// </summary>
255273
/// <returns>A deep copy.</returns>
256-
public MediaTypeHeaderValue Clone()
274+
public MediaTypeHeaderValue Copy()
257275
{
276+
if (IsReadOnly)
277+
{
278+
return this;
279+
}
280+
258281
var other = new MediaTypeHeaderValue();
259282
other._mediaType = _mediaType;
260283

261284
if (_parameters != null)
262285
{
263-
other._parameters = new ObjectCollection<NameValueHeaderValue>();
264-
foreach (var pair in _parameters)
265-
{
266-
other._parameters.Add(pair.Clone());
267-
}
286+
other._parameters = new ObjectCollection<NameValueHeaderValue>(
287+
_parameters.Select(item => item.Copy()));
288+
}
289+
return other;
290+
}
291+
292+
/// <summary>
293+
/// Performs a deep copy of this object and all of it's NameValueHeaderValue sub components,
294+
/// while avoiding the cost of revalidating the components. This copy is read-only.
295+
/// </summary>
296+
/// <returns>A deep, read-only, copy.</returns>
297+
public MediaTypeHeaderValue CopyAsReadOnly()
298+
{
299+
if (IsReadOnly)
300+
{
301+
return this;
302+
}
303+
304+
var other = new MediaTypeHeaderValue();
305+
other._mediaType = _mediaType;
306+
if (_parameters != null)
307+
{
308+
other._parameters = new ObjectCollection<NameValueHeaderValue>(
309+
_parameters.Select(item => item.CopyAsReadOnly()), isReadOnly: true);
268310
}
311+
other._isReadOnly = true;
269312
return other;
270313
}
271314

src/Microsoft.Net.Http.Headers/NameValueHeaderValue.cs

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ internal static readonly HttpHeaderParser<NameValueHeaderValue> MultipleValuePar
2020

2121
private string _name;
2222
private string _value;
23+
private bool _isReadOnly;
2324

2425
private NameValueHeaderValue()
2526
{
@@ -49,24 +50,47 @@ public string Value
4950
get { return _value; }
5051
set
5152
{
53+
HeaderUtilities.ThrowIfReadOnly(IsReadOnly);
5254
CheckValueFormat(value);
5355
_value = value;
5456
}
5557
}
5658

59+
public bool IsReadOnly { get { return _isReadOnly; } }
60+
5761
/// <summary>
5862
/// Provides a copy of this object without the cost of re-validating the values.
5963
/// </summary>
6064
/// <returns>A copy.</returns>
61-
public NameValueHeaderValue Clone()
65+
public NameValueHeaderValue Copy()
6266
{
67+
if (IsReadOnly)
68+
{
69+
return this;
70+
}
71+
6372
return new NameValueHeaderValue()
6473
{
6574
_name = _name,
6675
_value = _value
6776
};
6877
}
6978

79+
public NameValueHeaderValue CopyAsReadOnly()
80+
{
81+
if (IsReadOnly)
82+
{
83+
return this;
84+
}
85+
86+
return new NameValueHeaderValue()
87+
{
88+
_name = _name,
89+
_value = _value,
90+
_isReadOnly = true
91+
};
92+
}
93+
7094
public override int GetHashCode()
7195
{
7296
Contract.Assert(_name != null);

src/Microsoft.Net.Http.Headers/ObjectCollection.cs

Lines changed: 50 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

44
using System;
5+
using System.Collections;
6+
using System.Collections.Generic;
57
using System.Collections.ObjectModel;
68

79
namespace Microsoft.Net.Http.Headers
@@ -10,40 +12,74 @@ namespace Microsoft.Net.Http.Headers
1012
// type to throw if 'null' gets added. Collection<T> internally uses List<T> which comes at some cost. In addition
1113
// Collection<T>.Add() calls List<T>.InsertItem() which is an O(n) operation (compared to O(1) for List<T>.Add()).
1214
// This type is only used for very small collections (1-2 items) to keep the impact of using Collection<T> small.
13-
internal class ObjectCollection<T> : Collection<T> where T : class
15+
internal class ObjectCollection<T> : ICollection<T> where T : class
1416
{
15-
private static readonly Action<T> DefaultValidator = CheckNotNull;
17+
internal static readonly Action<T> DefaultValidator = CheckNotNull;
18+
internal static readonly ObjectCollection<T> EmptyReadOnlyCollection
19+
= new ObjectCollection<T>(DefaultValidator, isReadOnly: true);
1620

17-
private Action<T> _validator;
21+
private readonly Collection<T> _collection = new Collection<T>();
22+
private readonly Action<T> _validator;
23+
private readonly bool _isReadOnly;
1824

1925
public ObjectCollection()
2026
: this(DefaultValidator)
2127
{
2228
}
2329

24-
public ObjectCollection(Action<T> validator)
30+
public ObjectCollection(Action<T> validator, bool isReadOnly = false)
2531
{
2632
_validator = validator;
33+
_isReadOnly = isReadOnly;
2734
}
2835

29-
protected override void InsertItem(int index, T item)
36+
public ObjectCollection(IEnumerable<T> other, bool isReadOnly = false)
3037
{
31-
if (_validator != null)
38+
_validator = DefaultValidator;
39+
foreach (T item in other)
3240
{
33-
_validator(item);
41+
Add(item);
3442
}
35-
base.InsertItem(index, item);
43+
_isReadOnly = isReadOnly;
3644
}
3745

38-
protected override void SetItem(int index, T item)
46+
public int Count
3947
{
40-
if (_validator != null)
41-
{
42-
_validator(item);
43-
}
44-
base.SetItem(index, item);
48+
get { return _collection.Count; }
49+
}
50+
51+
public bool IsReadOnly
52+
{
53+
get { return _isReadOnly; }
54+
}
55+
56+
public void Add(T item)
57+
{
58+
HeaderUtilities.ThrowIfReadOnly(IsReadOnly);
59+
_validator(item);
60+
_collection.Add(item);
61+
}
62+
63+
public bool Remove(T item)
64+
{
65+
HeaderUtilities.ThrowIfReadOnly(IsReadOnly);
66+
return _collection.Remove(item);
67+
}
68+
69+
public void Clear()
70+
{
71+
HeaderUtilities.ThrowIfReadOnly(IsReadOnly);
72+
_collection.Clear();
4573
}
4674

75+
public bool Contains(T item) => _collection.Contains(item);
76+
77+
public void CopyTo(T[] array, int arrayIndex) => _collection.CopyTo(array, arrayIndex);
78+
79+
public IEnumerator<T> GetEnumerator() => _collection.GetEnumerator();
80+
81+
IEnumerator IEnumerable.GetEnumerator() => _collection.GetEnumerator();
82+
4783
private static void CheckNotNull(T item)
4884
{
4985
// null values cannot be added to the collection.

src/Microsoft.Net.Http.Headers/project.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
"System.Diagnostics.Contracts": "4.0.0-beta-*",
1313
"System.Globalization": "4.0.10-beta-*",
1414
"System.Globalization.Extensions": "4.0.0-beta-*",
15+
"System.Linq": "4.0.0-beta-*",
1516
"System.Text.Encoding": "4.0.10-beta-*",
1617
"System.Runtime": "4.0.20-beta-*"
1718
}

test/Microsoft.Framework.WebEncoders.Tests/project.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,5 +20,5 @@
2020
}
2121
}
2222
},
23-
"resources": "..\\..\\unicode\\UnicodeData.txt"
23+
"resource": "..\\..\\unicode\\UnicodeData.txt"
2424
}

test/Microsoft.Net.Http.Headers.Tests/MediaTypeHeaderValueTest.cs

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -65,22 +65,37 @@ public void Parameters_AddNull_Throw()
6565
}
6666

6767
[Fact]
68-
public void Clone_SimpleMediaType_Copied()
68+
public void Copy_SimpleMediaType_Copied()
6969
{
7070
var mediaType0 = new MediaTypeHeaderValue("text/plain");
71-
var mediaType1 = mediaType0.Clone();
71+
var mediaType1 = mediaType0.Copy();
7272
Assert.NotSame(mediaType0, mediaType1);
7373
Assert.Same(mediaType0.MediaType, mediaType1.MediaType);
7474
Assert.NotSame(mediaType0.Parameters, mediaType1.Parameters);
7575
Assert.Equal(mediaType0.Parameters.Count, mediaType1.Parameters.Count);
7676
}
7777

7878
[Fact]
79-
public void Clone_WithParameters_Copied()
79+
public void CopyAsReadOnly_SimpleMediaType_CopiedAndReadOnly()
80+
{
81+
var mediaType0 = new MediaTypeHeaderValue("text/plain");
82+
var mediaType1 = mediaType0.CopyAsReadOnly();
83+
Assert.NotSame(mediaType0, mediaType1);
84+
Assert.Same(mediaType0.MediaType, mediaType1.MediaType);
85+
Assert.NotSame(mediaType0.Parameters, mediaType1.Parameters);
86+
Assert.Equal(mediaType0.Parameters.Count, mediaType1.Parameters.Count);
87+
88+
Assert.False(mediaType0.IsReadOnly);
89+
Assert.True(mediaType1.IsReadOnly);
90+
Assert.Throws<InvalidOperationException>(() => { mediaType1.MediaType = "some/value"; });
91+
}
92+
93+
[Fact]
94+
public void Copy_WithParameters_Copied()
8095
{
8196
var mediaType0 = new MediaTypeHeaderValue("text/plain");
8297
mediaType0.Parameters.Add(new NameValueHeaderValue("name", "value"));
83-
var mediaType1 = mediaType0.Clone();
98+
var mediaType1 = mediaType0.Copy();
8499
Assert.NotSame(mediaType0, mediaType1);
85100
Assert.Same(mediaType0.MediaType, mediaType1.MediaType);
86101
Assert.NotSame(mediaType0.Parameters, mediaType1.Parameters);
@@ -92,6 +107,34 @@ public void Clone_WithParameters_Copied()
92107
Assert.Same(pair0.Value, pair1.Value);
93108
}
94109

110+
[Fact]
111+
public void CopyAsReadOnly_WithParameters_CopiedAndReadOnly()
112+
{
113+
var mediaType0 = new MediaTypeHeaderValue("text/plain");
114+
mediaType0.Parameters.Add(new NameValueHeaderValue("name", "value"));
115+
var mediaType1 = mediaType0.CopyAsReadOnly();
116+
Assert.NotSame(mediaType0, mediaType1);
117+
Assert.False(mediaType0.IsReadOnly);
118+
Assert.True(mediaType1.IsReadOnly);
119+
Assert.Same(mediaType0.MediaType, mediaType1.MediaType);
120+
121+
Assert.NotSame(mediaType0.Parameters, mediaType1.Parameters);
122+
Assert.False(mediaType0.Parameters.IsReadOnly);
123+
Assert.True(mediaType1.Parameters.IsReadOnly);
124+
Assert.Equal(mediaType0.Parameters.Count, mediaType1.Parameters.Count);
125+
Assert.Throws<InvalidOperationException>(() => mediaType1.Parameters.Add(new NameValueHeaderValue("name")));
126+
Assert.Throws<InvalidOperationException>(() => mediaType1.Parameters.Remove(new NameValueHeaderValue("name")));
127+
Assert.Throws<InvalidOperationException>(() => mediaType1.Parameters.Clear());
128+
129+
var pair0 = mediaType0.Parameters.First();
130+
var pair1 = mediaType1.Parameters.First();
131+
Assert.NotSame(pair0, pair1);
132+
Assert.False(pair0.IsReadOnly);
133+
Assert.True(pair1.IsReadOnly);
134+
Assert.Same(pair0.Name, pair1.Name);
135+
Assert.Same(pair0.Value, pair1.Value);
136+
}
137+
95138
[Fact]
96139
public void MediaType_SetAndGetMediaType_MatchExpectations()
97140
{

0 commit comments

Comments
 (0)