#177 Immutable HeaderValue objects.
This commit is contained in:
parent
12b78a31bf
commit
652d885402
|
|
@ -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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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-*"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,5 +20,5 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"resources": "..\\..\\unicode\\UnicodeData.txt"
|
||||
"resource": "..\\..\\unicode\\UnicodeData.txt"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
Loading…
Reference in New Issue