summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/DotNetOAuth.Test/DotNetOAuth.Test.csproj2
-rw-r--r--src/DotNetOAuth.Test/Messaging/CollectionAssert.cs19
-rw-r--r--src/DotNetOAuth.Test/Messaging/Reflection/MessageDictionaryTest.cs325
-rw-r--r--src/DotNetOAuth.Test/Mocks/TestBaseMessage.cs7
-rw-r--r--src/DotNetOAuth.Test/Mocks/TestDirectedMessage.cs7
-rw-r--r--src/DotNetOAuth.Test/Mocks/TestMessage.cs14
-rw-r--r--src/DotNetOAuth/DotNetOAuth.csproj5
-rw-r--r--src/DotNetOAuth/Messaging/IProtocolMessage.cs9
-rw-r--r--src/DotNetOAuth/Messaging/MessagePartAttribute.cs47
-rw-r--r--src/DotNetOAuth/Messaging/MessagingStrings.Designer.cs9
-rw-r--r--src/DotNetOAuth/Messaging/MessagingStrings.resx3
-rw-r--r--src/DotNetOAuth/Messaging/ProtocolException.cs13
-rw-r--r--src/DotNetOAuth/Messaging/Reflection/MessageDescription.cs56
-rw-r--r--src/DotNetOAuth/Messaging/Reflection/MessageDictionary.cs195
-rw-r--r--src/DotNetOAuth/Messaging/Reflection/MessagePart.cs91
-rw-r--r--src/DotNetOAuth/Messaging/Reflection/ValueMapping.cs27
16 files changed, 828 insertions, 1 deletions
diff --git a/src/DotNetOAuth.Test/DotNetOAuth.Test.csproj b/src/DotNetOAuth.Test/DotNetOAuth.Test.csproj
index 09b5974..028e1b9 100644
--- a/src/DotNetOAuth.Test/DotNetOAuth.Test.csproj
+++ b/src/DotNetOAuth.Test/DotNetOAuth.Test.csproj
@@ -58,6 +58,8 @@
</Reference>
</ItemGroup>
<ItemGroup>
+ <Compile Include="Messaging\CollectionAssert.cs" />
+ <Compile Include="Messaging\Reflection\MessageDictionaryTest.cs" />
<Compile Include="Messaging\MessagingTestBase.cs" />
<Compile Include="Messaging\MessagingUtilitiesTests.cs" />
<Compile Include="Messaging\ChannelTests.cs" />
diff --git a/src/DotNetOAuth.Test/Messaging/CollectionAssert.cs b/src/DotNetOAuth.Test/Messaging/CollectionAssert.cs
new file mode 100644
index 0000000..b9f3da5
--- /dev/null
+++ b/src/DotNetOAuth.Test/Messaging/CollectionAssert.cs
@@ -0,0 +1,19 @@
+//-----------------------------------------------------------------------
+// <copyright file="CollectionAssert.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOAuth.Test.Messaging {
+ using System.Collections;
+ using System.Collections.Generic;
+ using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+ internal class CollectionAssert<T> {
+ internal static void AreEquivalent(ICollection<T> expected, ICollection<T> actual) {
+ ICollection expectedNonGeneric = new List<T>(expected);
+ ICollection actualNonGeneric = new List<T>(actual);
+ CollectionAssert.AreEquivalent(expectedNonGeneric, actualNonGeneric);
+ }
+ }
+}
diff --git a/src/DotNetOAuth.Test/Messaging/Reflection/MessageDictionaryTest.cs b/src/DotNetOAuth.Test/Messaging/Reflection/MessageDictionaryTest.cs
new file mode 100644
index 0000000..6bbd849
--- /dev/null
+++ b/src/DotNetOAuth.Test/Messaging/Reflection/MessageDictionaryTest.cs
@@ -0,0 +1,325 @@
+//-----------------------------------------------------------------------
+// <copyright file="MessageDictionaryTest.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOAuth.Test.Messaging.Reflection {
+ using System;
+ using System.Collections;
+ using System.Collections.Generic;
+ using System.Collections.ObjectModel;
+ using DotNetOAuth.Messaging;
+ using DotNetOAuth.Messaging.Reflection;
+ using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+ [TestClass]
+ public class MessageDictionaryTest : MessagingTestBase {
+ private Mocks.TestMessage message;
+
+ [TestInitialize]
+ public override void SetUp() {
+ base.SetUp();
+
+ this.message = new Mocks.TestMessage();
+ }
+
+ [TestMethod, ExpectedException(typeof(ArgumentNullException))]
+ public void CtorNull() {
+ new MessageDictionary(null);
+ }
+
+ /// <summary>
+ /// A test for System.Collections.Generic.IDictionary&lt;System.String,System.String>.Values
+ /// </summary>
+ [TestMethod]
+ public void ValuesTest() {
+ IDictionary<string, string> target = new MessageDictionary(this.message);
+ Collection<string> expected = new Collection<string> {
+ this.message.Age.ToString(),
+ this.message.EmptyMember,
+ null, // this.message.Location.AbsoluteUri, (Location is null)
+ this.message.Name,
+ this.message.Timestamp.ToString(),
+ };
+ CollectionAssert<string>.AreEquivalent(expected, target.Values);
+
+ this.message.Age = 15;
+ this.message.Location = new Uri("http://localtest");
+ this.message.Name = "Andrew";
+ target["extra"] = "a";
+ expected = new Collection<string> {
+ this.message.Age.ToString(),
+ this.message.EmptyMember,
+ this.message.Location.AbsoluteUri,
+ this.message.Name,
+ this.message.Timestamp.ToString(),
+ "a",
+ };
+ CollectionAssert<string>.AreEquivalent(expected, target.Values);
+ }
+
+ /// <summary>
+ /// A test for System.Collections.Generic.IDictionary&lt;System.String,System.String>.Keys
+ /// </summary>
+ [TestMethod]
+ public void KeysTest() {
+ IDictionary<string, string> target = new MessageDictionary(this.message);
+ Collection<string> expected = new Collection<string> {
+ "age",
+ "EmptyMember",
+ "Location",
+ "Name",
+ "Timestamp",
+ };
+ CollectionAssert<string>.AreEquivalent(expected, target.Keys);
+
+ target["extraField"] = string.Empty;
+ expected.Add("extraField");
+ CollectionAssert<string>.AreEquivalent(expected, target.Keys);
+ }
+
+ /// <summary>
+ /// A test for System.Collections.Generic.IDictionary&lt;System.String,System.String>.Item
+ /// </summary>
+ [TestMethod]
+ public void ItemTest() {
+ IDictionary<string, string> target = new MessageDictionary(this.message);
+
+ // Test setting of declared message properties.
+ this.message.Age = 15;
+ Assert.AreEqual("15", target["age"]);
+ target["age"] = "13";
+ Assert.AreEqual(13, this.message.Age);
+
+ // Test setting extra fields
+ target["extra"] = "fun";
+ Assert.AreEqual("fun", target["extra"]);
+ Assert.AreEqual("fun", ((IProtocolMessage)this.message).ExtraData["extra"]);
+ }
+
+ /// <summary>
+ /// A test for System.Collections.Generic.ICollection&lt;System.Collections.Generic.KeyValuePair&lt;System.String,System.String&lt;&lt;.IsReadOnly
+ /// </summary>
+ [TestMethod]
+ public void IsReadOnlyTest() {
+ ICollection<KeyValuePair<string, string>> target = new MessageDictionary(this.message);
+ Assert.IsFalse(target.IsReadOnly);
+ }
+
+ /// <summary>
+ /// A test for System.Collections.Generic.ICollection&lt;System.Collections.Generic.KeyValuePair&lt;System.String,System.String&lt;&lt;.Count
+ /// </summary>
+ [TestMethod]
+ public void CountTest() {
+ ICollection<KeyValuePair<string, string>> target = new MessageDictionary(this.message);
+ IDictionary<string, string> targetDictionary = (IDictionary<string, string>)target;
+ Assert.AreEqual(targetDictionary.Keys.Count, target.Count);
+ targetDictionary["extraField"] = "hi";
+ Assert.AreEqual(targetDictionary.Keys.Count, target.Count);
+ }
+
+ /// <summary>
+ /// A test for System.Collections.Generic.IEnumerable&lt;System.Collections.Generic.KeyValuePair&lt;System.String,System.String&lt;&lt;.GetEnumerator
+ /// </summary>
+ [TestMethod]
+ public void GetEnumeratorTest() {
+ IEnumerable<KeyValuePair<string, string>> target = new MessageDictionary(this.message);
+ IDictionary<string, string> targetDictionary = (IDictionary<string, string>)target;
+ var keys = targetDictionary.Keys.GetEnumerator();
+ var values = targetDictionary.Values.GetEnumerator();
+ IEnumerator<KeyValuePair<string, string>> actual = target.GetEnumerator();
+
+ bool keysLast = true, valuesLast = true, actualLast = true;
+ while (true) {
+ keysLast = keys.MoveNext();
+ valuesLast = values.MoveNext();
+ actualLast = actual.MoveNext();
+ if (!keysLast || !valuesLast || !actualLast) {
+ break;
+ }
+
+ Assert.AreEqual(keys.Current, actual.Current.Key);
+ Assert.AreEqual(values.Current, actual.Current.Value);
+ }
+ Assert.IsTrue(keysLast == valuesLast && keysLast == actualLast);
+ }
+
+ [TestMethod]
+ public void GetEnumeratorUntypedTest() {
+ IEnumerable target = new MessageDictionary(this.message);
+ IDictionary<string, string> targetDictionary = (IDictionary<string, string>)target;
+ var keys = targetDictionary.Keys.GetEnumerator();
+ var values = targetDictionary.Values.GetEnumerator();
+ IEnumerator actual = target.GetEnumerator();
+
+ bool keysLast = true, valuesLast = true, actualLast = true;
+ while (true) {
+ keysLast = keys.MoveNext();
+ valuesLast = values.MoveNext();
+ actualLast = actual.MoveNext();
+ if (!keysLast || !valuesLast || !actualLast) {
+ break;
+ }
+
+ KeyValuePair<string, string> current = (KeyValuePair<string, string>)actual.Current;
+ Assert.AreEqual(keys.Current, current.Key);
+ Assert.AreEqual(values.Current, current.Value);
+ }
+ Assert.IsTrue(keysLast == valuesLast && keysLast == actualLast);
+ }
+
+ /// <summary>
+ /// A test for System.Collections.Generic.IDictionary&lt;System.String,System.String>.TryGetValue
+ /// </summary>
+ [TestMethod]
+ public void TryGetValueTest() {
+ IDictionary<string, string> target = new MessageDictionary(this.message);
+ this.message.Name = "andrew";
+ string name;
+ Assert.IsTrue(target.TryGetValue("Name", out name));
+ Assert.AreEqual(this.message.Name, name);
+
+ Assert.IsFalse(target.TryGetValue("name", out name));
+ Assert.IsNull(name);
+
+ target["extra"] = "value";
+ string extra;
+ Assert.IsTrue(target.TryGetValue("extra", out extra));
+ Assert.AreEqual("value", extra);
+ }
+
+ /// <summary>
+ /// A test for System.Collections.Generic.IDictionary&lt;System.String,System.String>.Remove
+ /// </summary>
+ [TestMethod]
+ public void RemoveTest1() {
+ IDictionary<string, string> target = new MessageDictionary(this.message);
+ this.message.Name = "andrew";
+ Assert.IsTrue(target.Remove("Name"));
+ Assert.IsNull(this.message.Name);
+ Assert.IsFalse(target.Remove("Name"));
+
+ Assert.IsFalse(target.Remove("extra"));
+ target["extra"] = "value";
+ Assert.IsTrue(target.Remove("extra"));
+ Assert.IsFalse(target.ContainsKey("extra"));
+ }
+
+ /// <summary>
+ /// A test for System.Collections.Generic.IDictionary&lt;System.String,System.String>.ContainsKey
+ /// </summary>
+ [TestMethod]
+ public void ContainsKeyTest() {
+ IDictionary<string, string> target = new MessageDictionary(this.message);
+ Assert.IsTrue(target.ContainsKey("age"), "Value type declared element should have a key.");
+ Assert.IsTrue(target.ContainsKey("Name"), "Null declared element should have a key.");
+ target.Remove("Name");
+ Assert.IsTrue(target.ContainsKey("Name"), "Removed declared element should still have a key.");
+
+ Assert.IsFalse(target.ContainsKey("extra"));
+ target["extra"] = "value";
+ Assert.IsTrue(target.ContainsKey("extra"));
+ }
+
+ /// <summary>
+ /// A test for System.Collections.Generic.IDictionary&lt;System.String,System.String&gt;.Add
+ /// </summary>
+ [TestMethod]
+ public void AddByKeyAndValueTest() {
+ IDictionary<string, string> target = new MessageDictionary(this.message);
+ target.Add("extra", "value");
+ Assert.IsTrue(target.Contains(new KeyValuePair<string, string>("extra", "value")));
+ target.Add("Name", "Andrew");
+ Assert.AreEqual("Andrew", this.message.Name);
+ }
+
+ /// <summary>
+ /// A test for System.Collections.Generic.ICollection&lt;System.Collections.Generic.KeyValuePair&lt;System.String,System.String&lt;&lt;.Add
+ /// </summary>
+ [TestMethod]
+ public void AddByKeyValuePairTest() {
+ IDictionary<string, string> target = new MessageDictionary(this.message);
+ target.Add(new KeyValuePair<string, string>("extra", "value"));
+ Assert.IsTrue(target.Contains(new KeyValuePair<string, string>("extra", "value")));
+ }
+
+ [TestMethod, ExpectedException(typeof(ArgumentException))]
+ public void AddExtraFieldThatAlreadyExistsTest() {
+ IDictionary<string, string> target = new MessageDictionary(this.message);
+ target.Add("extra", "value");
+ target.Add("extra", "value");
+ }
+
+ [TestMethod, ExpectedException(typeof(ArgumentException))]
+ public void AddDeclaredValueThatAlreadyExistsTest() {
+ IDictionary<string, string> target = new MessageDictionary(this.message);
+ target.Add("Name", "andrew");
+ target.Add("Name", "andrew");
+ }
+
+ /// <summary>
+ /// A test for System.Collections.Generic.ICollection&lt;System.Collections.Generic.KeyValuePair&lt;System.String,System.String&lt;&lt;.Remove
+ /// </summary>
+ [TestMethod]
+ public void RemoveByKeyValuePairTest() {
+ ICollection<KeyValuePair<string, string>> target = new MessageDictionary(this.message);
+ this.message.Name = "Andrew";
+ Assert.IsFalse(target.Remove(new KeyValuePair<string, string>("Name", "andrew")));
+ Assert.AreEqual("Andrew", this.message.Name);
+ Assert.IsTrue(target.Remove(new KeyValuePair<string, string>("Name", "Andrew")));
+ Assert.IsNull(this.message.Name);
+ }
+
+ /// <summary>
+ /// A test for System.Collections.Generic.ICollection&lt;System.Collections.Generic.KeyValuePair&lt;System.String,System.String&lt;&lt;.CopyTo
+ /// </summary>
+ [TestMethod]
+ public void CopyToTest() {
+ ICollection<KeyValuePair<string, string>> target = new MessageDictionary(this.message);
+ IDictionary<string, string> targetAsDictionary = ((IDictionary<string, string>)target);
+ KeyValuePair<string, string>[] array = new KeyValuePair<string, string>[target.Count + 1];
+ int arrayIndex = 1;
+ target.CopyTo(array, arrayIndex);
+ Assert.AreEqual(new KeyValuePair<string, string>(), array[0]);
+ for (int i = 1; i < array.Length; i++) {
+ Assert.IsNotNull(array[i].Key);
+ Assert.AreEqual(targetAsDictionary[array[i].Key], array[i].Value);
+ }
+ }
+
+ /// <summary>
+ /// A test for System.Collections.Generic.ICollection&lt;System.Collections.Generic.KeyValuePair&lt;System.String,System.String&lt;&lt;.Contains
+ /// </summary>
+ [TestMethod]
+ public void ContainsKeyValuePairTest() {
+ ICollection<KeyValuePair<string, string>> target = new MessageDictionary(this.message);
+ IDictionary<string, string> targetAsDictionary = ((IDictionary<string, string>)target);
+ Assert.IsFalse(target.Contains(new KeyValuePair<string, string>("age", "1")));
+ Assert.IsTrue(target.Contains(new KeyValuePair<string, string>("age", "0")));
+
+ targetAsDictionary["extra"] = "value";
+ Assert.IsFalse(target.Contains(new KeyValuePair<string, string>("extra", "Value")));
+ Assert.IsTrue(target.Contains(new KeyValuePair<string, string>("extra", "value")));
+ Assert.IsFalse(target.Contains(new KeyValuePair<string, string>("wayoff", "value")));
+ }
+
+ /// <summary>
+ /// A test for System.Collections.Generic.ICollection&lt;System.Collections.Generic.KeyValuePair&lt;System.String,System.String&lt;&lt;.Clear
+ /// </summary>
+ [TestMethod]
+ public void ClearTest() {
+ ICollection<KeyValuePair<string, string>> target = new MessageDictionary(this.message);
+ IDictionary<string, string> targetAsDictionary = ((IDictionary<string, string>)target);
+ this.message.Name = "Andrew";
+ this.message.Age = 15;
+ targetAsDictionary["extra"] = "value";
+ int countBeforeClear = targetAsDictionary.Count;
+ target.Clear();
+ Assert.AreEqual(countBeforeClear - 1, target.Count, "Clearing with one extra parameter should reduce count by 1.");
+ Assert.IsFalse(targetAsDictionary.ContainsKey("extra"));
+ Assert.IsNull(this.message.Name);
+ Assert.AreEqual(0, this.message.Age);
+ }
+ }
+}
diff --git a/src/DotNetOAuth.Test/Mocks/TestBaseMessage.cs b/src/DotNetOAuth.Test/Mocks/TestBaseMessage.cs
index 29e2809..8f500cf 100644
--- a/src/DotNetOAuth.Test/Mocks/TestBaseMessage.cs
+++ b/src/DotNetOAuth.Test/Mocks/TestBaseMessage.cs
@@ -6,6 +6,7 @@
namespace DotNetOAuth.Test.Mocks {
using System;
+ using System.Collections.Generic;
using System.Runtime.Serialization;
using DotNetOAuth.Messaging;
@@ -15,6 +16,8 @@ namespace DotNetOAuth.Test.Mocks {
[DataContract(Namespace = Protocol.DataContractNamespaceV10)]
internal class TestBaseMessage : IProtocolMessage, IBaseMessageExplicitMembers {
+ private Dictionary<string, string> extraData = new Dictionary<string, string>();
+
[DataMember(Name = "age", IsRequired = true)]
public int Age { get; set; }
@@ -36,6 +39,10 @@ namespace DotNetOAuth.Test.Mocks {
get { return MessageTransport.Indirect; }
}
+ IDictionary<string, string> IProtocolMessage.ExtraData {
+ get { return this.extraData; }
+ }
+
internal string PrivatePropertyAccessor {
get { return this.PrivateProperty; }
set { this.PrivateProperty = value; }
diff --git a/src/DotNetOAuth.Test/Mocks/TestDirectedMessage.cs b/src/DotNetOAuth.Test/Mocks/TestDirectedMessage.cs
index 7add28b..cdd130d 100644
--- a/src/DotNetOAuth.Test/Mocks/TestDirectedMessage.cs
+++ b/src/DotNetOAuth.Test/Mocks/TestDirectedMessage.cs
@@ -6,6 +6,7 @@
namespace DotNetOAuth.Test.Mocks {
using System;
+ using System.Collections.Generic;
using System.Runtime.Serialization;
using DotNetOAuth.Messaging;
@@ -13,6 +14,8 @@ namespace DotNetOAuth.Test.Mocks {
internal class TestDirectedMessage : IDirectedProtocolMessage {
private MessageTransport transport;
+ private Dictionary<string, string> extraData = new Dictionary<string, string>();
+
internal TestDirectedMessage(MessageTransport transport) {
this.transport = transport;
}
@@ -46,6 +49,10 @@ namespace DotNetOAuth.Test.Mocks {
get { return this.transport; }
}
+ IDictionary<string, string> IProtocolMessage.ExtraData {
+ get { return this.extraData; }
+ }
+
#endregion
protected virtual MessageProtection RequiredProtection {
diff --git a/src/DotNetOAuth.Test/Mocks/TestMessage.cs b/src/DotNetOAuth.Test/Mocks/TestMessage.cs
index e67b582..1a9840d 100644
--- a/src/DotNetOAuth.Test/Mocks/TestMessage.cs
+++ b/src/DotNetOAuth.Test/Mocks/TestMessage.cs
@@ -6,14 +6,17 @@
namespace DotNetOAuth.Test.Mocks {
using System;
+ using System.Collections.Generic;
using System.Runtime.Serialization;
using DotNetOAuth.Messaging;
[DataContract(Namespace = Protocol.DataContractNamespaceV10)]
internal class TestMessage : IProtocolMessage {
private MessageTransport transport;
+ private Dictionary<string, string> extraData = new Dictionary<string, string>();
- internal TestMessage() : this(MessageTransport.Direct) {
+ internal TestMessage()
+ : this(MessageTransport.Direct) {
}
internal TestMessage(MessageTransport transport) {
@@ -21,14 +24,19 @@ namespace DotNetOAuth.Test.Mocks {
}
[DataMember(Name = "age", IsRequired = true)]
+ [MessagePart("age")]
public int Age { get; set; }
[DataMember]
+ [MessagePart(Optional = true)]
public string Name { get; set; }
[DataMember]
+ [MessagePart(Optional = true)]
public string EmptyMember { get; set; }
[DataMember]
+ [MessagePart(Optional = true)]
public Uri Location { get; set; }
[DataMember]
+ [MessagePart(Optional = true)]
public DateTime Timestamp { get; set; }
#region IProtocolMessage Members
@@ -45,6 +53,10 @@ namespace DotNetOAuth.Test.Mocks {
get { return this.transport; }
}
+ IDictionary<string, string> IProtocolMessage.ExtraData {
+ get { return this.extraData; }
+ }
+
void IProtocolMessage.EnsureValidMessage() {
if (this.EmptyMember != null || this.Age < 0) {
throw new ProtocolException();
diff --git a/src/DotNetOAuth/DotNetOAuth.csproj b/src/DotNetOAuth/DotNetOAuth.csproj
index 16f0dac..c53f1e0 100644
--- a/src/DotNetOAuth/DotNetOAuth.csproj
+++ b/src/DotNetOAuth/DotNetOAuth.csproj
@@ -68,6 +68,7 @@
<ItemGroup>
<Compile Include="Consumer.cs" />
<Compile Include="IWebRequestHandler.cs" />
+ <Compile Include="Messaging\MessagePartAttribute.cs" />
<Compile Include="Messaging\MessageProtection.cs" />
<Compile Include="Messaging\IChannelBindingElement.cs" />
<Compile Include="Messaging\Bindings\ReplayedMessageException.cs" />
@@ -90,6 +91,10 @@
</Compile>
<Compile Include="Messaging\MessagingUtilities.cs" />
<Compile Include="Messaging\Bindings\StandardExpirationBindingElement.cs" />
+ <Compile Include="Messaging\Reflection\ValueMapping.cs" />
+ <Compile Include="Messaging\Reflection\MessageDescription.cs" />
+ <Compile Include="Messaging\Reflection\MessageDictionary.cs" />
+ <Compile Include="Messaging\Reflection\MessagePart.cs" />
<Compile Include="Messaging\UnprotectedMessageException.cs" />
<Compile Include="OAuthChannel.cs" />
<Compile Include="Messaging\Response.cs" />
diff --git a/src/DotNetOAuth/Messaging/IProtocolMessage.cs b/src/DotNetOAuth/Messaging/IProtocolMessage.cs
index 61df813..a95822c 100644
--- a/src/DotNetOAuth/Messaging/IProtocolMessage.cs
+++ b/src/DotNetOAuth/Messaging/IProtocolMessage.cs
@@ -30,6 +30,15 @@ namespace DotNetOAuth.Messaging {
MessageTransport Transport { get; }
/// <summary>
+ /// Gets the dictionary of additional name/value fields tacked on to this message.
+ /// </summary>
+ /// <remarks>
+ /// Implementations of <see cref="IProtocolMessage"/> should ensure that this property
+ /// never returns null.
+ /// </remarks>
+ IDictionary<string, string> ExtraData { get; }
+
+ /// <summary>
/// Checks the message state for conformity to the protocol specification
/// and throws an exception if the message is invalid.
/// </summary>
diff --git a/src/DotNetOAuth/Messaging/MessagePartAttribute.cs b/src/DotNetOAuth/Messaging/MessagePartAttribute.cs
new file mode 100644
index 0000000..6620f7f
--- /dev/null
+++ b/src/DotNetOAuth/Messaging/MessagePartAttribute.cs
@@ -0,0 +1,47 @@
+//-----------------------------------------------------------------------
+// <copyright file="MessagePartAttribute.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOAuth.Messaging {
+ using System;
+ using System.Net.Security;
+ using System.Reflection;
+
+ [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, Inherited = true, AllowMultiple = false)]
+ internal sealed class MessagePartAttribute : Attribute {
+ private bool initialized;
+ private string name;
+
+ internal MessagePartAttribute() {
+ }
+
+ internal MessagePartAttribute(string name) {
+ this.Name = name;
+ }
+
+ public string Name {
+ get { return this.name; }
+ set { this.name = string.IsNullOrEmpty(value) ? null : value; }
+ }
+
+ public ProtectionLevel Signed { get; set; }
+
+ public bool Optional { get; set; }
+
+ internal void Initialize(MemberInfo member) {
+ if (member == null) {
+ throw new ArgumentNullException("member");
+ }
+
+ if (!this.initialized) {
+ if (String.IsNullOrEmpty(this.Name)) {
+ this.Name = member.Name;
+ }
+
+ this.initialized = true;
+ }
+ }
+ }
+}
diff --git a/src/DotNetOAuth/Messaging/MessagingStrings.Designer.cs b/src/DotNetOAuth/Messaging/MessagingStrings.Designer.cs
index 49e726d..7852ea2 100644
--- a/src/DotNetOAuth/Messaging/MessagingStrings.Designer.cs
+++ b/src/DotNetOAuth/Messaging/MessagingStrings.Designer.cs
@@ -151,6 +151,15 @@ namespace DotNetOAuth.Messaging {
}
/// <summary>
+ /// Looks up a localized string similar to An item with the same key has already been added..
+ /// </summary>
+ internal static string KeyAlreadyExists {
+ get {
+ return ResourceManager.GetString("KeyAlreadyExists", resourceCulture);
+ }
+ }
+
+ /// <summary>
/// Looks up a localized string similar to A message response is already queued for sending in the response stream..
/// </summary>
internal static string QueuedMessageResponseAlreadyExists {
diff --git a/src/DotNetOAuth/Messaging/MessagingStrings.resx b/src/DotNetOAuth/Messaging/MessagingStrings.resx
index 2133c44..121f9b8 100644
--- a/src/DotNetOAuth/Messaging/MessagingStrings.resx
+++ b/src/DotNetOAuth/Messaging/MessagingStrings.resx
@@ -147,6 +147,9 @@
<data name="InsufficentMessageProtection" xml:space="preserve">
<value>The message required protections {0} but the channel could only apply {1}.</value>
</data>
+ <data name="KeyAlreadyExists" xml:space="preserve">
+ <value>An item with the same key has already been added.</value>
+ </data>
<data name="QueuedMessageResponseAlreadyExists" xml:space="preserve">
<value>A message response is already queued for sending in the response stream.</value>
</data>
diff --git a/src/DotNetOAuth/Messaging/ProtocolException.cs b/src/DotNetOAuth/Messaging/ProtocolException.cs
index 05ff340..d8b3d33 100644
--- a/src/DotNetOAuth/Messaging/ProtocolException.cs
+++ b/src/DotNetOAuth/Messaging/ProtocolException.cs
@@ -6,6 +6,7 @@
namespace DotNetOAuth.Messaging {
using System;
+ using System.Collections.Generic;
/// <summary>
/// An exception to represent errors in the local or remote implementation of the protocol.
@@ -23,6 +24,11 @@ namespace DotNetOAuth.Messaging {
private Uri recipient;
/// <summary>
+ /// A cache for extra name/value pairs tacked on as data when this exception is sent as a message.
+ /// </summary>
+ private Dictionary<string, string> extraData = new Dictionary<string, string>();
+
+ /// <summary>
/// Initializes a new instance of the <see cref="ProtocolException"/> class.
/// </summary>
public ProtocolException() { }
@@ -148,6 +154,13 @@ namespace DotNetOAuth.Messaging {
}
}
+ /// <summary>
+ /// Gets the dictionary of additional name/value fields tacked on to this message.
+ /// </summary>
+ IDictionary<string, string> IProtocolMessage.ExtraData {
+ get { return this.extraData; }
+ }
+
#endregion
/// <summary>
diff --git a/src/DotNetOAuth/Messaging/Reflection/MessageDescription.cs b/src/DotNetOAuth/Messaging/Reflection/MessageDescription.cs
new file mode 100644
index 0000000..ef73a31
--- /dev/null
+++ b/src/DotNetOAuth/Messaging/Reflection/MessageDescription.cs
@@ -0,0 +1,56 @@
+//-----------------------------------------------------------------------
+// <copyright file="MessageDescription.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOAuth.Messaging.Reflection {
+ using System;
+ using System.Collections.Generic;
+ using System.Linq;
+ using System.Reflection;
+
+ internal class MessageDescription {
+ private Type messageType;
+ private Dictionary<string, MessagePart> mapping;
+
+ internal MessageDescription(Type messageType) {
+ if (messageType == null) {
+ throw new ArgumentNullException("messageType");
+ }
+
+ if (!typeof(IProtocolMessage).IsAssignableFrom(messageType)) {
+ throw new ArgumentOutOfRangeException(); // TODO: better message
+ }
+
+ this.messageType = messageType;
+ this.ReflectMessageType();
+ }
+
+ internal Type MessageType {
+ get { return this.messageType; }
+ }
+
+ internal IDictionary<string, MessagePart> Mapping {
+ get { return this.mapping; }
+ }
+
+ internal void ReflectMessageType() {
+ this.mapping = new Dictionary<string, MessagePart>();
+
+ Type currentType = this.messageType;
+ do {
+ foreach (MemberInfo member in currentType.GetMembers(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.DeclaredOnly)) {
+ if (member is PropertyInfo || member is FieldInfo) {
+ MessagePartAttribute partAttribute = member.GetCustomAttributes(typeof(MessagePartAttribute), true).OfType<MessagePartAttribute>().FirstOrDefault();
+ if (partAttribute != null) {
+ MessagePart part = new MessagePart(member, partAttribute);
+ this.mapping.Add(part.Name, part);
+ }
+ }
+ }
+ currentType = currentType.BaseType;
+ } while (currentType != null);
+ }
+ }
+}
diff --git a/src/DotNetOAuth/Messaging/Reflection/MessageDictionary.cs b/src/DotNetOAuth/Messaging/Reflection/MessageDictionary.cs
new file mode 100644
index 0000000..eca7a9b
--- /dev/null
+++ b/src/DotNetOAuth/Messaging/Reflection/MessageDictionary.cs
@@ -0,0 +1,195 @@
+//-----------------------------------------------------------------------
+// <copyright file="MessageDictionary.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOAuth.Messaging.Reflection {
+ using System;
+ using System.Collections;
+ using System.Collections.Generic;
+
+ /// <summary>
+ /// Wraps an <see cref="IProtocolMessage"/> instance in a dictionary that
+ /// provides access to both well-defined message properties and "extra"
+ /// name/value pairs that have no properties associated with them.
+ /// </summary>
+ internal class MessageDictionary : IDictionary<string, string> {
+ private IProtocolMessage message;
+
+ private MessageDescription description;
+
+ internal MessageDictionary(IProtocolMessage message) {
+ if (message == null) {
+ throw new ArgumentNullException("message");
+ }
+
+ this.message = message;
+ this.description = new MessageDescription(message.GetType());
+ }
+
+ #region IDictionary<string,string> Members
+
+ void IDictionary<string, string>.Add(string key, string value) {
+ MessagePart part;
+ if (this.description.Mapping.TryGetValue(key, out part)) {
+ if (part.GetValue(this.message) != null) {
+ throw new ArgumentException(MessagingStrings.KeyAlreadyExists);
+ }
+ part.SetValue(this.message, value);
+ } else {
+ this.message.ExtraData.Add(key, value);
+ }
+ }
+
+ bool IDictionary<string, string>.ContainsKey(string key) {
+ return this.message.ExtraData.ContainsKey(key) || this.description.Mapping.ContainsKey(key);
+ }
+
+ ICollection<string> IDictionary<string, string>.Keys {
+ get {
+ string[] keys = new string[this.message.ExtraData.Count + this.description.Mapping.Count];
+ int i = 0;
+ foreach (string key in this.description.Mapping.Keys) {
+ keys[i++] = key;
+ }
+
+ foreach (string key in this.message.ExtraData.Keys) {
+ keys[i++] = key;
+ }
+
+ return keys;
+ }
+ }
+
+ bool IDictionary<string, string>.Remove(string key) {
+ if (this.message.ExtraData.Remove(key)) {
+ return true;
+ } else {
+ MessagePart part;
+ if (this.description.Mapping.TryGetValue(key, out part)) {
+ if (part.GetValue(this.message) != null) {
+ part.SetValue(this.message, null);
+ return true;
+ }
+ }
+ return false;
+ }
+ }
+
+ bool IDictionary<string, string>.TryGetValue(string key, out string value) {
+ MessagePart part;
+ if (this.description.Mapping.TryGetValue(key, out part)) {
+ value = part.GetValue(this.message);
+ return true;
+ }
+ return this.message.ExtraData.TryGetValue(key, out value);
+ }
+
+ ICollection<string> IDictionary<string, string>.Values {
+ get {
+ string[] values = new string[this.message.ExtraData.Count + this.description.Mapping.Count];
+ int i = 0;
+ foreach (MessagePart part in this.description.Mapping.Values) {
+ values[i++] = part.GetValue(this.message);
+ }
+
+ foreach (string value in this.message.ExtraData.Values) {
+ values[i++] = value;
+ }
+
+ return values;
+ }
+ }
+
+ string IDictionary<string, string>.this[string key] {
+ get {
+ MessagePart part;
+ if (this.description.Mapping.TryGetValue(key, out part)) {
+ return part.GetValue(this.message);
+ } else {
+ return this.message.ExtraData[key];
+ }
+ }
+
+ set {
+ MessagePart part;
+ if (this.description.Mapping.TryGetValue(key, out part)) {
+ part.SetValue(this.message, value);
+ } else {
+ this.message.ExtraData[key] = value;
+ }
+ }
+ }
+
+ #endregion
+
+ #region ICollection<KeyValuePair<string,string>> Members
+
+ void ICollection<KeyValuePair<string, string>>.Add(KeyValuePair<string, string> item) {
+ ((IDictionary<string, string>)this).Add(item.Key, item.Value);
+ }
+
+ void ICollection<KeyValuePair<string, string>>.Clear() {
+ foreach (string key in ((IDictionary<string, string>)this).Keys) {
+ ((IDictionary<string, string>)this).Remove(key);
+ }
+ }
+
+ bool ICollection<KeyValuePair<string, string>>.Contains(KeyValuePair<string, string> item) {
+ MessagePart part;
+ if (this.description.Mapping.TryGetValue(item.Key, out part)) {
+ return string.Equals(part.GetValue(this.message), item.Value, StringComparison.Ordinal);
+ } else {
+ return this.message.ExtraData.Contains(item);
+ }
+ }
+
+ void ICollection<KeyValuePair<string, string>>.CopyTo(KeyValuePair<string, string>[] array, int arrayIndex) {
+ foreach (var pair in (IDictionary<string, string>)this) {
+ array[arrayIndex++] = pair;
+ }
+ }
+
+ int ICollection<KeyValuePair<string, string>>.Count {
+ get { return this.description.Mapping.Count + this.message.ExtraData.Count; }
+ }
+
+ bool ICollection<KeyValuePair<string, string>>.IsReadOnly {
+ get { return false; }
+ }
+
+ bool ICollection<KeyValuePair<string, string>>.Remove(KeyValuePair<string, string> item) {
+ // We use contains because that checks that the value is equal as well.
+ if (((ICollection<KeyValuePair<string, string>>)this).Contains(item)) {
+ ((IDictionary<string, string>)this).Remove(item.Key);
+ return true;
+ }
+ return false;
+ }
+
+ #endregion
+
+ #region IEnumerable<KeyValuePair<string,string>> Members
+
+ IEnumerator<KeyValuePair<string, string>> IEnumerable<KeyValuePair<string, string>>.GetEnumerator() {
+ foreach (MessagePart part in this.description.Mapping.Values) {
+ yield return new KeyValuePair<string, string>(part.Name, part.GetValue(this.message));
+ }
+
+ foreach (var pair in this.message.ExtraData) {
+ yield return pair;
+ }
+ }
+
+ #endregion
+
+ #region IEnumerable Members
+
+ IEnumerator System.Collections.IEnumerable.GetEnumerator() {
+ return ((IEnumerable<KeyValuePair<string, string>>)this).GetEnumerator();
+ }
+
+ #endregion
+ }
+}
diff --git a/src/DotNetOAuth/Messaging/Reflection/MessagePart.cs b/src/DotNetOAuth/Messaging/Reflection/MessagePart.cs
new file mode 100644
index 0000000..09f959b
--- /dev/null
+++ b/src/DotNetOAuth/Messaging/Reflection/MessagePart.cs
@@ -0,0 +1,91 @@
+//-----------------------------------------------------------------------
+// <copyright file="MessagePart.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOAuth.Messaging.Reflection {
+ using System;
+ using System.Collections.Generic;
+ using System.Net.Security;
+ using System.Reflection;
+
+ internal class MessagePart {
+ private static readonly Dictionary<Type, ValueMapping> converters = new Dictionary<Type, ValueMapping>();
+
+ private ValueMapping converter;
+
+ private PropertyInfo property;
+
+ private FieldInfo field;
+
+ static MessagePart() {
+ Map<Uri>(uri => uri.AbsoluteUri, str => new Uri(str));
+ }
+
+ internal MessagePart(MemberInfo member, MessagePartAttribute attribute) {
+ if (member == null) {
+ throw new ArgumentNullException("member");
+ }
+
+ this.field = member as FieldInfo;
+ this.property = member as PropertyInfo;
+ if (this.field == null && this.property == null) {
+ throw new ArgumentOutOfRangeException("member"); // TODO: add descriptive message
+ }
+
+ if (attribute == null) {
+ throw new ArgumentNullException("attribute");
+ }
+
+ this.Name = attribute.Name ?? member.Name;
+ this.Signed = attribute.Signed;
+ this.IsRequired = !attribute.Optional;
+
+ if (!converters.TryGetValue(member.DeclaringType, out this.converter)) {
+ Type memberDeclaredType = (this.field != null) ? this.field.FieldType : this.property.PropertyType;
+ this.converter = new ValueMapping(
+ obj => obj != null ? obj.ToString() : null,
+ str => str != null ? Convert.ChangeType(str, memberDeclaredType) : null);
+ }
+ }
+
+ internal string Name { get; set; }
+
+ internal ProtectionLevel Signed { get; set; }
+
+ internal bool IsRequired { get; set; }
+
+ internal object ToValue(string value) {
+ return this.converter.StringToValue(value);
+ }
+
+ internal string ToString(object value) {
+ return this.converter.ValueToString(value);
+ }
+
+ internal void SetValue(IProtocolMessage message, string value) {
+ if (this.property != null) {
+ this.property.SetValue(message, this.ToValue(value), null);
+ } else {
+ this.field.SetValue(message, this.ToValue(value));
+ }
+ }
+
+ internal string GetValue(IProtocolMessage message) {
+ if (this.property != null) {
+ return this.ToString(this.property.GetValue(message, null));
+ } else {
+ return this.ToString(this.field.GetValue(message));
+ }
+ }
+
+ private static void Map<T>(Func<T, string> toString, Func<string, T> toValue) where T : class {
+ converters.Add(
+ typeof(T),
+ new ValueMapping(
+ obj => obj != null ? toString((T)obj) : null,
+ str => str != null ? toValue(str) : null));
+ }
+ }
+}
diff --git a/src/DotNetOAuth/Messaging/Reflection/ValueMapping.cs b/src/DotNetOAuth/Messaging/Reflection/ValueMapping.cs
new file mode 100644
index 0000000..2371b49
--- /dev/null
+++ b/src/DotNetOAuth/Messaging/Reflection/ValueMapping.cs
@@ -0,0 +1,27 @@
+//-----------------------------------------------------------------------
+// <copyright file="ValueMapping.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOAuth.Messaging.Reflection {
+ using System;
+
+ internal struct ValueMapping {
+ internal Func<object, string> ValueToString;
+ internal Func<string, object> StringToValue;
+
+ internal ValueMapping(Func<object, string> toString, Func<string, object> toValue) {
+ if (toString == null) {
+ throw new ArgumentNullException("toString");
+ }
+
+ if (toValue == null) {
+ throw new ArgumentNullException("toValue");
+ }
+
+ this.ValueToString = toString;
+ this.StringToValue = toValue;
+ }
+ }
+}