Added support for message types to contain const fields to represent message parts...
[dotnetoauth.git] / src / DotNetOpenAuth / Messaging / Reflection / MessagePart.cs
blob3a1432a801ca341f19694e401401681f8cb60d0d
1 //-----------------------------------------------------------------------
2 // <copyright file="MessagePart.cs" company="Andrew Arnott">
3 // Copyright (c) Andrew Arnott. All rights reserved.
4 // </copyright>
5 //-----------------------------------------------------------------------
7 namespace DotNetOpenAuth.Messaging.Reflection {
8 using System;
9 using System.Collections.Generic;
10 using System.Diagnostics.CodeAnalysis;
11 using System.Globalization;
12 using System.Net.Security;
13 using System.Reflection;
14 using System.Xml;
15 using DotNetOpenAuth.OpenId;
17 /// <summary>
18 /// Describes an individual member of a message and assists in its serialization.
19 /// </summary>
20 internal class MessagePart {
21 /// <summary>
22 /// A map of converters that help serialize custom objects to string values and back again.
23 /// </summary>
24 private static readonly Dictionary<Type, ValueMapping> converters = new Dictionary<Type, ValueMapping>();
26 /// <summary>
27 /// A map of instantiated custom encoders used to encode/decode message parts.
28 /// </summary>
29 private static readonly Dictionary<Type, IMessagePartEncoder> encoders = new Dictionary<Type, IMessagePartEncoder>();
31 /// <summary>
32 /// The string-object conversion routines to use for this individual message part.
33 /// </summary>
34 private ValueMapping converter;
36 /// <summary>
37 /// The property that this message part is associated with, if aplicable.
38 /// </summary>
39 private PropertyInfo property;
41 /// <summary>
42 /// The field that this message part is associated with, if aplicable.
43 /// </summary>
44 private FieldInfo field;
46 /// <summary>
47 /// The type of the message part. (Not the type of the message itself).
48 /// </summary>
49 private Type memberDeclaredType;
51 /// <summary>
52 /// The default (uninitialized) value of the member inherent in its type.
53 /// </summary>
54 private object defaultMemberValue;
56 /// <summary>
57 /// Initializes static members of the <see cref="MessagePart"/> class.
58 /// </summary>
59 [SuppressMessage("Microsoft.Performance", "CA1810:InitializeReferenceTypeStaticFieldsInline", Justification = "Much more efficient initialization when we can call methods.")]
60 static MessagePart() {
61 Map<Uri>(uri => uri.AbsoluteUri, str => new Uri(str));
62 Map<DateTime>(dt => XmlConvert.ToString(dt, XmlDateTimeSerializationMode.Utc), str => XmlConvert.ToDateTime(str, XmlDateTimeSerializationMode.Utc));
63 Map<byte[]>(bytes => Convert.ToBase64String(bytes), str => Convert.FromBase64String(str));
64 Map<Realm>(realm => realm.ToString(), str => new Realm(str));
65 Map<Identifier>(id => id.ToString(), str => Identifier.Parse(str));
68 /// <summary>
69 /// Initializes a new instance of the <see cref="MessagePart"/> class.
70 /// </summary>
71 /// <param name="member">
72 /// A property or field of an <see cref="IMessage"/> implementing type
73 /// that has a <see cref="MessagePartAttribute"/> attached to it.
74 /// </param>
75 /// <param name="attribute">
76 /// The attribute discovered on <paramref name="member"/> that describes the
77 /// serialization requirements of the message part.
78 /// </param>
79 internal MessagePart(MemberInfo member, MessagePartAttribute attribute) {
80 if (member == null) {
81 throw new ArgumentNullException("member");
84 this.field = member as FieldInfo;
85 this.property = member as PropertyInfo;
86 if (this.field == null && this.property == null) {
87 throw new ArgumentException(
88 string.Format(
89 CultureInfo.CurrentCulture,
90 MessagingStrings.UnexpectedType,
91 typeof(FieldInfo).Name + ", " + typeof(PropertyInfo).Name,
92 member.GetType().Name),
93 "member");
96 if (attribute == null) {
97 throw new ArgumentNullException("attribute");
100 this.Name = attribute.Name ?? member.Name;
101 this.RequiredProtection = attribute.RequiredProtection;
102 this.IsRequired = attribute.IsRequired;
103 this.AllowEmpty = attribute.AllowEmpty;
104 this.memberDeclaredType = (this.field != null) ? this.field.FieldType : this.property.PropertyType;
105 this.defaultMemberValue = DeriveDefaultValue(this.memberDeclaredType);
107 if (attribute.Encoder == null) {
108 if (!converters.TryGetValue(this.memberDeclaredType, out this.converter)) {
109 this.converter = new ValueMapping(
110 obj => obj != null ? obj.ToString() : null,
111 str => str != null ? Convert.ChangeType(str, this.memberDeclaredType, CultureInfo.InvariantCulture) : null);
113 } else {
114 var encoder = GetEncoder(attribute.Encoder);
115 this.converter = new ValueMapping(
116 obj => encoder.Encode(obj),
117 str => encoder.Decode(str));
120 // readonly and const fields are considered legal, and "constants" for message transport.
121 FieldAttributes constAttributes = FieldAttributes.Static | FieldAttributes.Literal | FieldAttributes.HasDefault;
122 if (this.field != null && (
123 (this.field.Attributes & FieldAttributes.InitOnly) == FieldAttributes.InitOnly ||
124 (this.field.Attributes & constAttributes) == constAttributes)) {
125 this.IsConstantValue = true;
126 } else if (this.property != null && !this.property.CanWrite) {
127 this.IsConstantValue = true;
130 // Validate a sane combination of settings
131 this.ValidateSettings();
134 /// <summary>
135 /// Gets or sets the name to use when serializing or deserializing this parameter in a message.
136 /// </summary>
137 internal string Name { get; set; }
139 /// <summary>
140 /// Gets or sets whether this message part must be signed.
141 /// </summary>
142 internal ProtectionLevel RequiredProtection { get; set; }
144 /// <summary>
145 /// Gets or sets a value indicating whether this message part is required for the
146 /// containing message to be valid.
147 /// </summary>
148 internal bool IsRequired { get; set; }
150 /// <summary>
151 /// Gets or sets a value indicating whether the string value is allowed to be empty in the serialized message.
152 /// </summary>
153 internal bool AllowEmpty { get; set; }
155 /// <summary>
156 /// Gets or sets a value indicating whether the field or property must remain its default value.
157 /// </summary>
158 internal bool IsConstantValue { get; set; }
160 /// <summary>
161 /// Sets the member of a given message to some given value.
162 /// Used in deserialization.
163 /// </summary>
164 /// <param name="message">The message instance containing the member whose value should be set.</param>
165 /// <param name="value">The string representation of the value to set.</param>
166 internal void SetValue(IMessage message, string value) {
167 if (message == null) {
168 throw new ArgumentNullException("message");
171 try {
172 if (this.IsConstantValue) {
173 string constantValue = this.GetValue(message);
174 if (!string.Equals(constantValue, value)) {
175 throw new ArgumentException(string.Format(
176 CultureInfo.CurrentCulture,
177 MessagingStrings.UnexpectedMessagePartValueForConstant,
178 message.GetType().Name,
179 this.Name,
180 constantValue,
181 value));
183 } else {
184 if (this.property != null) {
185 this.property.SetValue(message, this.ToValue(value), null);
186 } else {
187 this.field.SetValue(message, this.ToValue(value));
190 } catch (FormatException ex) {
191 throw ErrorUtilities.Wrap(ex, MessagingStrings.MessagePartReadFailure, message.GetType(), this.Name, value);
195 /// <summary>
196 /// Gets the value of a member of a given message.
197 /// Used in serialization.
198 /// </summary>
199 /// <param name="message">The message instance to read the value from.</param>
200 /// <returns>The string representation of the member's value.</returns>
201 internal string GetValue(IMessage message) {
202 try {
203 object value = this.GetValueAsObject(message);
204 return this.ToString(value);
205 } catch (FormatException ex) {
206 throw ErrorUtilities.Wrap(ex, MessagingStrings.MessagePartWriteFailure, message.GetType(), this.Name);
210 /// <summary>
211 /// Gets whether the value has been set to something other than its CLR type default value.
212 /// </summary>
213 /// <param name="message">The message instance to check the value on.</param>
214 /// <returns>True if the value is not the CLR default value.</returns>
215 internal bool IsNondefaultValueSet(IMessage message) {
216 if (this.memberDeclaredType.IsValueType) {
217 return !this.GetValueAsObject(message).Equals(this.defaultMemberValue);
218 } else {
219 return this.defaultMemberValue != this.GetValueAsObject(message);
223 /// <summary>
224 /// Figures out the CLR default value for a given type.
225 /// </summary>
226 /// <param name="type">The type whose default value is being sought.</param>
227 /// <returns>Either null, or some default value like 0 or 0.0.</returns>
228 private static object DeriveDefaultValue(Type type) {
229 if (type.IsValueType) {
230 return Activator.CreateInstance(type);
231 } else {
232 return null;
236 /// <summary>
237 /// Adds a pair of type conversion functions to the static converstion map.
238 /// </summary>
239 /// <typeparam name="T">The custom type to convert to and from strings.</typeparam>
240 /// <param name="toString">The function to convert the custom type to a string.</param>
241 /// <param name="toValue">The function to convert a string to the custom type.</param>
242 private static void Map<T>(Func<T, string> toString, Func<string, T> toValue) {
243 Func<object, string> safeToString = obj => obj != null ? toString((T)obj) : null;
244 Func<string, object> safeToT = str => str != null ? toValue(str) : default(T);
245 converters.Add(typeof(T), new ValueMapping(safeToString, safeToT));
248 /// <summary>
249 /// Checks whether a type is a nullable value type (i.e. int?)
250 /// </summary>
251 /// <param name="type">The type in question.</param>
252 /// <returns>True if this is a nullable value type.</returns>
253 private static bool IsNonNullableValueType(Type type) {
254 if (!type.IsValueType) {
255 return false;
258 if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>)) {
259 return false;
262 return true;
265 /// <summary>
266 /// Retrieves a previously instantiated encoder of a given type, or creates a new one and stores it for later retrieval as well.
267 /// </summary>
268 /// <param name="messagePartEncoder">The message part encoder type.</param>
269 /// <returns>An instance of the desired encoder.</returns>
270 private static IMessagePartEncoder GetEncoder(Type messagePartEncoder) {
271 IMessagePartEncoder encoder;
272 if (!encoders.TryGetValue(messagePartEncoder, out encoder)) {
273 encoder = encoders[messagePartEncoder] = (IMessagePartEncoder)Activator.CreateInstance(messagePartEncoder);
276 return encoder;
279 /// <summary>
280 /// Converts a string representation of the member's value to the appropriate type.
281 /// </summary>
282 /// <param name="value">The string representation of the member's value.</param>
283 /// <returns>
284 /// An instance of the appropriate type for setting the member.
285 /// </returns>
286 private object ToValue(string value) {
287 return value == null ? null : this.converter.StringToValue(value);
290 /// <summary>
291 /// Converts the member's value to its string representation.
292 /// </summary>
293 /// <param name="value">The value of the member.</param>
294 /// <returns>
295 /// The string representation of the member's value.
296 /// </returns>
297 private string ToString(object value) {
298 return value == null ? null : this.converter.ValueToString(value);
301 /// <summary>
302 /// Gets the value of the message part, without converting it to/from a string.
303 /// </summary>
304 /// <param name="message">The message instance to read from.</param>
305 /// <returns>The value of the member.</returns>
306 private object GetValueAsObject(IMessage message) {
307 if (this.property != null) {
308 return this.property.GetValue(message, null);
309 } else {
310 return this.field.GetValue(message);
314 /// <summary>
315 /// Validates that the message part and its attribute have agreeable settings.
316 /// </summary>
317 /// <exception cref="ArgumentException">
318 /// Thrown when a non-nullable value type is set as optional.
319 /// </exception>
320 private void ValidateSettings() {
321 if (!this.IsRequired && IsNonNullableValueType(this.memberDeclaredType)) {
322 MemberInfo member = (MemberInfo)this.field ?? this.property;
323 throw new ArgumentException(
324 string.Format(
325 CultureInfo.CurrentCulture,
326 "Invalid combination: {0} on message type {1} is a non-nullable value type but is marked as optional.",
327 member.Name,
328 member.DeclaringType));