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

View File

@ -20,6 +20,7 @@ namespace Microsoft.Net.Http.Headers
private string _name;
private string _value;
private bool _isReadOnly;
private NameValueHeaderValue()
{
@ -49,17 +50,25 @@ namespace Microsoft.Net.Http.Headers
get { return _value; }
set
{
HeaderUtilities.ThrowIfReadOnly(IsReadOnly);
CheckValueFormat(value);
_value = value;
}
}
public bool IsReadOnly { get { return _isReadOnly; } }
/// <summary>
/// Provides a copy of this object without the cost of re-validating the values.
/// </summary>
/// <returns>A copy.</returns>
public NameValueHeaderValue Clone()
public NameValueHeaderValue Copy()
{
if (IsReadOnly)
{
return this;
}
return new NameValueHeaderValue()
{
_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()
{
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.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
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
// 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.
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()
: this(DefaultValidator)
{
}
public ObjectCollection(Action<T> validator)
public ObjectCollection(Action<T> validator, bool isReadOnly = false)
{
_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)
{
_validator(item);
}
base.SetItem(index, item);
get { return _collection.Count; }
}
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)
{
// null values cannot be added to the collection.

View File

@ -12,6 +12,7 @@
"System.Diagnostics.Contracts": "4.0.0-beta-*",
"System.Globalization": "4.0.10-beta-*",
"System.Globalization.Extensions": "4.0.0-beta-*",
"System.Linq": "4.0.0-beta-*",
"System.Text.Encoding": "4.0.10-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]
public void Clone_SimpleMediaType_Copied()
public void Copy_SimpleMediaType_Copied()
{
var mediaType0 = new MediaTypeHeaderValue("text/plain");
var mediaType1 = mediaType0.Clone();
var mediaType1 = mediaType0.Copy();
Assert.NotSame(mediaType0, mediaType1);
Assert.Same(mediaType0.MediaType, mediaType1.MediaType);
Assert.NotSame(mediaType0.Parameters, mediaType1.Parameters);
@ -76,11 +76,26 @@ namespace Microsoft.Net.Http.Headers
}
[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");
mediaType0.Parameters.Add(new NameValueHeaderValue("name", "value"));
var mediaType1 = mediaType0.Clone();
var mediaType1 = mediaType0.Copy();
Assert.NotSame(mediaType0, mediaType1);
Assert.Same(mediaType0.MediaType, mediaType1.MediaType);
Assert.NotSame(mediaType0.Parameters, mediaType1.Parameters);
@ -92,6 +107,34 @@ namespace Microsoft.Net.Http.Headers
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]
public void MediaType_SetAndGetMediaType_MatchExpectations()
{

View File

@ -60,29 +60,71 @@ namespace Microsoft.Net.Http.Headers
}
[Fact]
public void Clone_NameOnly_SuccesfullyCopied()
public void Copy_NameOnly_SuccesfullyCopied()
{
var pair0 = new NameValueHeaderValue("name");
var pair1 = pair0.Clone();
var pair1 = pair0.Copy();
Assert.NotSame(pair0, pair1);
Assert.Same(pair0.Name, pair1.Name);
Assert.Null(pair0.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]
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 pair1 = pair0.Clone();
var pair1 = pair0.Copy();
Assert.NotSame(pair0, pair1);
Assert.Same(pair0.Name, pair1.Name);
Assert.Same(pair0.Value, pair1.Value);
// Change one value and verify the other is unchanged.
pair1.Value = "othervalue";
Assert.Equal("value", pair0.Value);
Assert.Equal("othervalue", pair1.Value);
pair0.Value = "othervalue";
Assert.Equal("othervalue", pair0.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]