1
//-----------------------------------------------------------------------
2 // <copyright file="MessagePart.cs" company="Andrew Arnott">
3 // Copyright (c) Andrew Arnott. All rights reserved.
5 //-----------------------------------------------------------------------
7 namespace DotNetOpenAuth
.Messaging
.Reflection
{
9 using System
.Collections
.Generic
;
10 using System
.Diagnostics
.CodeAnalysis
;
11 using System
.Globalization
;
12 using System
.Net
.Security
;
13 using System
.Reflection
;
15 using DotNetOpenAuth
.OpenId
;
18 /// Describes an individual member of a message and assists in its serialization.
20 internal class MessagePart
{
22 /// A map of converters that help serialize custom objects to string values and back again.
24 private static readonly Dictionary
<Type
, ValueMapping
> converters
= new Dictionary
<Type
, ValueMapping
>();
27 /// A map of instantiated custom encoders used to encode/decode message parts.
29 private static readonly Dictionary
<Type
, IMessagePartEncoder
> encoders
= new Dictionary
<Type
, IMessagePartEncoder
>();
32 /// The string-object conversion routines to use for this individual message part.
34 private ValueMapping converter
;
37 /// The property that this message part is associated with, if aplicable.
39 private PropertyInfo property
;
42 /// The field that this message part is associated with, if aplicable.
44 private FieldInfo field
;
47 /// The type of the message part. (Not the type of the message itself).
49 private Type memberDeclaredType
;
52 /// The default (uninitialized) value of the member inherent in its type.
54 private object defaultMemberValue
;
57 /// Initializes static members of the <see cref="MessagePart"/> class.
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
));
69 /// Initializes a new instance of the <see cref="MessagePart"/> class.
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.
75 /// <param name="attribute">
76 /// The attribute discovered on <paramref name="member"/> that describes the
77 /// serialization requirements of the message part.
79 internal MessagePart(MemberInfo member
, MessagePartAttribute attribute
) {
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(
89 CultureInfo
.CurrentCulture
,
90 MessagingStrings
.UnexpectedType
,
91 typeof(FieldInfo
).Name
+ ", " + typeof(PropertyInfo
).Name
,
92 member
.GetType().Name
),
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);
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();
135 /// Gets or sets the name to use when serializing or deserializing this parameter in a message.
137 internal string Name { get; set; }
140 /// Gets or sets whether this message part must be signed.
142 internal ProtectionLevel RequiredProtection { get; set; }
145 /// Gets or sets a value indicating whether this message part is required for the
146 /// containing message to be valid.
148 internal bool IsRequired { get; set; }
151 /// Gets or sets a value indicating whether the string value is allowed to be empty in the serialized message.
153 internal bool AllowEmpty { get; set; }
156 /// Gets or sets a value indicating whether the field or property must remain its default value.
158 internal bool IsConstantValue { get; set; }
161 /// Sets the member of a given message to some given value.
162 /// Used in deserialization.
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");
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
,
184 if (this.property
!= null) {
185 this.property
.SetValue(message
, this.ToValue(value), null);
187 this.field
.SetValue(message
, this.ToValue(value));
190 } catch (FormatException ex
) {
191 throw ErrorUtilities
.Wrap(ex
, MessagingStrings
.MessagePartReadFailure
, message
.GetType(), this.Name
, value);
196 /// Gets the value of a member of a given message.
197 /// Used in serialization.
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
) {
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
);
211 /// Gets whether the value has been set to something other than its CLR type default value.
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
);
219 return this.defaultMemberValue
!= this.GetValueAsObject(message
);
224 /// Figures out the CLR default value for a given type.
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
);
237 /// Adds a pair of type conversion functions to the static converstion map.
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
));
249 /// Checks whether a type is a nullable value type (i.e. int?)
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
) {
258 if (type
.IsGenericType
&& type
.GetGenericTypeDefinition() == typeof(Nullable
<>)) {
266 /// Retrieves a previously instantiated encoder of a given type, or creates a new one and stores it for later retrieval as well.
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
);
280 /// Converts a string representation of the member's value to the appropriate type.
282 /// <param name="value">The string representation of the member's value.</param>
284 /// An instance of the appropriate type for setting the member.
286 private object ToValue(string value) {
287 return value == null ? null : this.converter
.StringToValue(value);
291 /// Converts the member's value to its string representation.
293 /// <param name="value">The value of the member.</param>
295 /// The string representation of the member's value.
297 private string ToString(object value) {
298 return value == null ? null : this.converter
.ValueToString(value);
302 /// Gets the value of the message part, without converting it to/from a string.
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);
310 return this.field
.GetValue(message
);
315 /// Validates that the message part and its attribute have agreeable settings.
317 /// <exception cref="ArgumentException">
318 /// Thrown when a non-nullable value type is set as optional.
320 private void ValidateSettings() {
321 if (!this.IsRequired
&& IsNonNullableValueType(this.memberDeclaredType
)) {
322 MemberInfo member
= (MemberInfo
)this.field
?? this.property
;
323 throw new ArgumentException(
325 CultureInfo
.CurrentCulture
,
326 "Invalid combination: {0} on message type {1} is a non-nullable value type but is marked as optional.",
328 member
.DeclaringType
));