#177 Immutable HeaderValue objects.

This commit is contained in:
Chris R 2015-06-25 09:34:14 -07:00
parent 12b78a31bf
commit 652d885402
8 changed files with 233 additions and 36 deletions

View File

@ -228,5 +228,13 @@ namespace Microsoft.Net.Http.Headers
} }
return input; return input;
} }
internal static void ThrowIfReadOnly(bool isReadOnly)
{
if (isReadOnly)
{
throw new InvalidOperationException("The object cannot be modified because it is read-only.");
}
}
} }
} }

View File

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.Contracts; using System.Diagnostics.Contracts;
using System.Globalization; using System.Globalization;
using System.Linq;
using System.Text; using System.Text;
namespace Microsoft.Net.Http.Headers namespace Microsoft.Net.Http.Headers
@ -19,9 +20,10 @@ namespace Microsoft.Net.Http.Headers
private static readonly HttpHeaderParser<MediaTypeHeaderValue> MultipleValueParser private static readonly HttpHeaderParser<MediaTypeHeaderValue> MultipleValueParser
= new GenericHeaderParser<MediaTypeHeaderValue>(true, GetMediaTypeLength); = new GenericHeaderParser<MediaTypeHeaderValue>(true, GetMediaTypeLength);
// Use list instead of dictionary since we may have multiple parameters with the same name. // Use a collection instead of a dictionary since we may have multiple parameters with the same name.
private ICollection<NameValueHeaderValue> _parameters; private ObjectCollection<NameValueHeaderValue> _parameters;
private string _mediaType; private string _mediaType;
private bool _isReadOnly;
private MediaTypeHeaderValue() private MediaTypeHeaderValue()
{ {
@ -48,6 +50,7 @@ namespace Microsoft.Net.Http.Headers
} }
set set
{ {
HeaderUtilities.ThrowIfReadOnly(IsReadOnly);
// We don't prevent a user from setting whitespace-only charsets. Like we can't prevent a user from // We don't prevent a user from setting whitespace-only charsets. Like we can't prevent a user from
// setting a non-existing charset. // setting a non-existing charset.
var charsetParameter = NameValueHeaderValue.Find(_parameters, CharsetString); var charsetParameter = NameValueHeaderValue.Find(_parameters, CharsetString);
@ -93,6 +96,7 @@ namespace Microsoft.Net.Http.Headers
} }
set set
{ {
HeaderUtilities.ThrowIfReadOnly(IsReadOnly);
if (value == null) if (value == null)
{ {
Charset = null; Charset = null;
@ -112,6 +116,7 @@ namespace Microsoft.Net.Http.Headers
} }
set set
{ {
HeaderUtilities.ThrowIfReadOnly(IsReadOnly);
var boundaryParameter = NameValueHeaderValue.Find(_parameters, BoundaryString); var boundaryParameter = NameValueHeaderValue.Find(_parameters, BoundaryString);
if (string.IsNullOrEmpty(value)) if (string.IsNullOrEmpty(value))
{ {
@ -141,7 +146,14 @@ namespace Microsoft.Net.Http.Headers
{ {
if (_parameters == null) if (_parameters == null)
{ {
_parameters = new ObjectCollection<NameValueHeaderValue>(); if (IsReadOnly)
{
_parameters = ObjectCollection<NameValueHeaderValue>.EmptyReadOnlyCollection;
}
else
{
_parameters = new ObjectCollection<NameValueHeaderValue>();
}
} }
return _parameters; return _parameters;
} }
@ -158,6 +170,7 @@ namespace Microsoft.Net.Http.Headers
get { return _mediaType; } get { return _mediaType; }
set set
{ {
HeaderUtilities.ThrowIfReadOnly(IsReadOnly);
CheckMediaTypeFormat(value, "value"); CheckMediaTypeFormat(value, "value");
_mediaType = value; _mediaType = value;
} }
@ -201,6 +214,11 @@ namespace Microsoft.Net.Http.Headers
} }
} }
public bool IsReadOnly
{
get { return _isReadOnly; }
}
public bool IsSubsetOf(MediaTypeHeaderValue otherMediaType) public bool IsSubsetOf(MediaTypeHeaderValue otherMediaType)
{ {
if (otherMediaType == null) if (otherMediaType == null)
@ -253,22 +271,47 @@ namespace Microsoft.Net.Http.Headers
/// while avoiding the cost of revalidating the components. /// while avoiding the cost of revalidating the components.
/// </summary> /// </summary>
/// <returns>A deep copy.</returns> /// <returns>A deep copy.</returns>
public MediaTypeHeaderValue Clone() public MediaTypeHeaderValue Copy()
{ {
if (IsReadOnly)
{
return this;
}
var other = new MediaTypeHeaderValue(); var other = new MediaTypeHeaderValue();
other._mediaType = _mediaType; other._mediaType = _mediaType;
if (_parameters != null) if (_parameters != null)
{ {
other._parameters = new ObjectCollection<NameValueHeaderValue>(); other._parameters = new ObjectCollection<NameValueHeaderValue>(
foreach (var pair in _parameters) _parameters.Select(item => item.Copy()));
{
other._parameters.Add(pair.Clone());
}
} }
return other; return other;
} }
/// <summary>
/// Performs a deep copy of this object and all of it's NameValueHeaderValue sub components,
/// while avoiding the cost of revalidating the components. This copy is read-only.
/// </summary>
/// <returns>A deep, read-only, copy.</returns>
public MediaTypeHeaderValue CopyAsReadOnly()
{
if (IsReadOnly)
{
return this;
}
var other = new MediaTypeHeaderValue();
other._mediaType = _mediaType;
if (_parameters != null)
{
other._parameters = new ObjectCollection<NameValueHeaderValue>(
_parameters.Select(item => item.CopyAsReadOnly()), isReadOnly: true);
}
other._isReadOnly = true;
return other;
}
public override string ToString() public override string ToString()
{ {
return _mediaType + NameValueHeaderValue.ToString(_parameters, ';', true); return _mediaType + NameValueHeaderValue.ToString(_parameters, ';', true);

View File

@ -20,6 +20,7 @@ namespace Microsoft.Net.Http.Headers
private string _name; private string _name;
private string _value; private string _value;
private bool _isReadOnly;
private NameValueHeaderValue() private NameValueHeaderValue()
{ {
@ -49,17 +50,25 @@ namespace Microsoft.Net.Http.Headers
get { return _value; } get { return _value; }
set set
{ {
HeaderUtilities.ThrowIfReadOnly(IsReadOnly);
CheckValueFormat(value); CheckValueFormat(value);
_value = value; _value = value;
} }
} }
public bool IsReadOnly { get { return _isReadOnly; } }
/// <summary> /// <summary>
/// Provides a copy of this object without the cost of re-validating the values. /// Provides a copy of this object without the cost of re-validating the values.
/// </summary> /// </summary>
/// <returns>A copy.</returns> /// <returns>A copy.</returns>
public NameValueHeaderValue Clone() public NameValueHeaderValue Copy()
{ {
if (IsReadOnly)
{
return this;
}
return new NameValueHeaderValue() return new NameValueHeaderValue()
{ {
_name = _name, _name = _name,
@ -67,6 +76,21 @@ namespace Microsoft.Net.Http.Headers
}; };
} }
public NameValueHeaderValue CopyAsReadOnly()
{
if (IsReadOnly)
{
return this;
}
return new NameValueHeaderValue()
{
_name = _name,
_value = _value,
_isReadOnly = true
};
}
public override int GetHashCode() public override int GetHashCode()
{ {
Contract.Assert(_name != null); Contract.Assert(_name != null);

View File

@ -2,6 +2,8 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
namespace Microsoft.Net.Http.Headers namespace Microsoft.Net.Http.Headers
@ -10,40 +12,74 @@ namespace Microsoft.Net.Http.Headers
// type to throw if 'null' gets added. Collection<T> internally uses List<T> which comes at some cost. In addition // type to throw if 'null' gets added. Collection<T> internally uses List<T> which comes at some cost. In addition
// Collection<T>.Add() calls List<T>.InsertItem() which is an O(n) operation (compared to O(1) for List<T>.Add()). // Collection<T>.Add() calls List<T>.InsertItem() which is an O(n) operation (compared to O(1) for List<T>.Add()).
// This type is only used for very small collections (1-2 items) to keep the impact of using Collection<T> small. // This type is only used for very small collections (1-2 items) to keep the impact of using Collection<T> small.
internal class ObjectCollection<T> : Collection<T> where T : class internal class ObjectCollection<T> : ICollection<T> where T : class
{ {
private static readonly Action<T> DefaultValidator = CheckNotNull; internal static readonly Action<T> DefaultValidator = CheckNotNull;
internal static readonly ObjectCollection<T> EmptyReadOnlyCollection
= new ObjectCollection<T>(DefaultValidator, isReadOnly: true);
private Action<T> _validator; private readonly Collection<T> _collection = new Collection<T>();
private readonly Action<T> _validator;
private readonly bool _isReadOnly;
public ObjectCollection() public ObjectCollection()
: this(DefaultValidator) : this(DefaultValidator)
{ {
} }
public ObjectCollection(Action<T> validator) public ObjectCollection(Action<T> validator, bool isReadOnly = false)
{ {
_validator = validator; _validator = validator;
_isReadOnly = isReadOnly;
} }
protected override void InsertItem(int index, T item) public ObjectCollection(IEnumerable<T> other, bool isReadOnly = false)
{ {
if (_validator != null) _validator = DefaultValidator;
foreach (T item in other)
{ {
_validator(item); Add(item);
} }
base.InsertItem(index, item); _isReadOnly = isReadOnly;
} }
protected override void SetItem(int index, T item) public int Count
{ {
if (_validator != null) get { return _collection.Count; }
{
_validator(item);
}
base.SetItem(index, item);
} }
public bool IsReadOnly
{
get { return _isReadOnly; }
}
public void Add(T item)
{
HeaderUtilities.ThrowIfReadOnly(IsReadOnly);
_validator(item);
_collection.Add(item);
}
public bool Remove(T item)
{
HeaderUtilities.ThrowIfReadOnly(IsReadOnly);
return _collection.Remove(item);
}
public void Clear()
{
HeaderUtilities.ThrowIfReadOnly(IsReadOnly);
_collection.Clear();
}
public bool Contains(T item) => _collection.Contains(item);
public void CopyTo(T[] array, int arrayIndex) => _collection.CopyTo(array, arrayIndex);
public IEnumerator<T> GetEnumerator() => _collection.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => _collection.GetEnumerator();
private static void CheckNotNull(T item) private static void CheckNotNull(T item)
{ {
// null values cannot be added to the collection. // null values cannot be added to the collection.

View File

@ -12,6 +12,7 @@
"System.Diagnostics.Contracts": "4.0.0-beta-*", "System.Diagnostics.Contracts": "4.0.0-beta-*",
"System.Globalization": "4.0.10-beta-*", "System.Globalization": "4.0.10-beta-*",
"System.Globalization.Extensions": "4.0.0-beta-*", "System.Globalization.Extensions": "4.0.0-beta-*",
"System.Linq": "4.0.0-beta-*",
"System.Text.Encoding": "4.0.10-beta-*", "System.Text.Encoding": "4.0.10-beta-*",
"System.Runtime": "4.0.20-beta-*" "System.Runtime": "4.0.20-beta-*"
} }

View File

@ -20,5 +20,5 @@
} }
} }
}, },
"resources": "..\\..\\unicode\\UnicodeData.txt" "resource": "..\\..\\unicode\\UnicodeData.txt"
} }

View File

@ -65,10 +65,10 @@ namespace Microsoft.Net.Http.Headers
} }
[Fact] [Fact]
public void Clone_SimpleMediaType_Copied() public void Copy_SimpleMediaType_Copied()
{ {
var mediaType0 = new MediaTypeHeaderValue("text/plain"); var mediaType0 = new MediaTypeHeaderValue("text/plain");
var mediaType1 = mediaType0.Clone(); var mediaType1 = mediaType0.Copy();
Assert.NotSame(mediaType0, mediaType1); Assert.NotSame(mediaType0, mediaType1);
Assert.Same(mediaType0.MediaType, mediaType1.MediaType); Assert.Same(mediaType0.MediaType, mediaType1.MediaType);
Assert.NotSame(mediaType0.Parameters, mediaType1.Parameters); Assert.NotSame(mediaType0.Parameters, mediaType1.Parameters);
@ -76,11 +76,26 @@ namespace Microsoft.Net.Http.Headers
} }
[Fact] [Fact]
public void Clone_WithParameters_Copied() public void CopyAsReadOnly_SimpleMediaType_CopiedAndReadOnly()
{
var mediaType0 = new MediaTypeHeaderValue("text/plain");
var mediaType1 = mediaType0.CopyAsReadOnly();
Assert.NotSame(mediaType0, mediaType1);
Assert.Same(mediaType0.MediaType, mediaType1.MediaType);
Assert.NotSame(mediaType0.Parameters, mediaType1.Parameters);
Assert.Equal(mediaType0.Parameters.Count, mediaType1.Parameters.Count);
Assert.False(mediaType0.IsReadOnly);
Assert.True(mediaType1.IsReadOnly);
Assert.Throws<InvalidOperationException>(() => { mediaType1.MediaType = "some/value"; });
}
[Fact]
public void Copy_WithParameters_Copied()
{ {
var mediaType0 = new MediaTypeHeaderValue("text/plain"); var mediaType0 = new MediaTypeHeaderValue("text/plain");
mediaType0.Parameters.Add(new NameValueHeaderValue("name", "value")); mediaType0.Parameters.Add(new NameValueHeaderValue("name", "value"));
var mediaType1 = mediaType0.Clone(); var mediaType1 = mediaType0.Copy();
Assert.NotSame(mediaType0, mediaType1); Assert.NotSame(mediaType0, mediaType1);
Assert.Same(mediaType0.MediaType, mediaType1.MediaType); Assert.Same(mediaType0.MediaType, mediaType1.MediaType);
Assert.NotSame(mediaType0.Parameters, mediaType1.Parameters); Assert.NotSame(mediaType0.Parameters, mediaType1.Parameters);
@ -92,6 +107,34 @@ namespace Microsoft.Net.Http.Headers
Assert.Same(pair0.Value, pair1.Value); Assert.Same(pair0.Value, pair1.Value);
} }
[Fact]
public void CopyAsReadOnly_WithParameters_CopiedAndReadOnly()
{
var mediaType0 = new MediaTypeHeaderValue("text/plain");
mediaType0.Parameters.Add(new NameValueHeaderValue("name", "value"));
var mediaType1 = mediaType0.CopyAsReadOnly();
Assert.NotSame(mediaType0, mediaType1);
Assert.False(mediaType0.IsReadOnly);
Assert.True(mediaType1.IsReadOnly);
Assert.Same(mediaType0.MediaType, mediaType1.MediaType);
Assert.NotSame(mediaType0.Parameters, mediaType1.Parameters);
Assert.False(mediaType0.Parameters.IsReadOnly);
Assert.True(mediaType1.Parameters.IsReadOnly);
Assert.Equal(mediaType0.Parameters.Count, mediaType1.Parameters.Count);
Assert.Throws<InvalidOperationException>(() => mediaType1.Parameters.Add(new NameValueHeaderValue("name")));
Assert.Throws<InvalidOperationException>(() => mediaType1.Parameters.Remove(new NameValueHeaderValue("name")));
Assert.Throws<InvalidOperationException>(() => mediaType1.Parameters.Clear());
var pair0 = mediaType0.Parameters.First();
var pair1 = mediaType1.Parameters.First();
Assert.NotSame(pair0, pair1);
Assert.False(pair0.IsReadOnly);
Assert.True(pair1.IsReadOnly);
Assert.Same(pair0.Name, pair1.Name);
Assert.Same(pair0.Value, pair1.Value);
}
[Fact] [Fact]
public void MediaType_SetAndGetMediaType_MatchExpectations() public void MediaType_SetAndGetMediaType_MatchExpectations()
{ {

View File

@ -60,29 +60,71 @@ namespace Microsoft.Net.Http.Headers
} }
[Fact] [Fact]
public void Clone_NameOnly_SuccesfullyCopied() public void Copy_NameOnly_SuccesfullyCopied()
{ {
var pair0 = new NameValueHeaderValue("name"); var pair0 = new NameValueHeaderValue("name");
var pair1 = pair0.Clone(); var pair1 = pair0.Copy();
Assert.NotSame(pair0, pair1); Assert.NotSame(pair0, pair1);
Assert.Same(pair0.Name, pair1.Name); Assert.Same(pair0.Name, pair1.Name);
Assert.Null(pair0.Value); Assert.Null(pair0.Value);
Assert.Null(pair1.Value); Assert.Null(pair1.Value);
// Change one value and verify the other is unchanged.
pair0.Value = "othervalue";
Assert.Equal("othervalue", pair0.Value);
Assert.Equal(null, pair1.Value);
} }
[Fact] [Fact]
public void Clone_NameAndValue_SuccesfullyCopied() public void CopyAsReadOnly_NameOnly_CopiedAndReadOnly()
{
var pair0 = new NameValueHeaderValue("name");
var pair1 = pair0.CopyAsReadOnly();
Assert.NotSame(pair0, pair1);
Assert.Same(pair0.Name, pair1.Name);
Assert.Null(pair0.Value);
Assert.Null(pair1.Value);
Assert.False(pair0.IsReadOnly);
Assert.True(pair1.IsReadOnly);
// Change one value and verify the other is unchanged.
pair0.Value = "othervalue";
Assert.Equal("othervalue", pair0.Value);
Assert.Equal(null, pair1.Value);
Assert.Throws<InvalidOperationException>(() => { pair1.Value = "othervalue"; });
}
[Fact]
public void Copy_NameAndValue_SuccesfullyCopied()
{ {
var pair0 = new NameValueHeaderValue("name", "value"); var pair0 = new NameValueHeaderValue("name", "value");
var pair1 = pair0.Clone(); var pair1 = pair0.Copy();
Assert.NotSame(pair0, pair1); Assert.NotSame(pair0, pair1);
Assert.Same(pair0.Name, pair1.Name); Assert.Same(pair0.Name, pair1.Name);
Assert.Same(pair0.Value, pair1.Value); Assert.Same(pair0.Value, pair1.Value);
// Change one value and verify the other is unchanged. // Change one value and verify the other is unchanged.
pair1.Value = "othervalue"; pair0.Value = "othervalue";
Assert.Equal("value", pair0.Value); Assert.Equal("othervalue", pair0.Value);
Assert.Equal("othervalue", pair1.Value); Assert.Equal("value", pair1.Value);
}
[Fact]
public void CopyAsReadOnly_NameAndValue_CopiedAndReadOnly()
{
var pair0 = new NameValueHeaderValue("name", "value");
var pair1 = pair0.CopyAsReadOnly();
Assert.NotSame(pair0, pair1);
Assert.Same(pair0.Name, pair1.Name);
Assert.Same(pair0.Value, pair1.Value);
Assert.False(pair0.IsReadOnly);
Assert.True(pair1.IsReadOnly);
// Change one value and verify the other is unchanged.
pair0.Value = "othervalue";
Assert.Equal("othervalue", pair0.Value);
Assert.Equal("value", pair1.Value);
Assert.Throws<InvalidOperationException>(() => { pair1.Value = "othervalue"; });
} }
[Fact] [Fact]