Dyna.cs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357
  1. using System;
  2. using System.Collections.Concurrent;
  3. using System.Collections.Generic;
  4. using System.Linq;
  5. namespace Quadarax.Foundation.Core.Object.Dynamic
  6. {
  7. /// <summary>
  8. /// Provides a dynamic object implementation that allows for runtime definition and manipulation of properties.
  9. /// </summary>
  10. /// <remarks>
  11. /// The Dyna class offers a flexible way to create objects with dynamic property sets that can be
  12. /// defined at runtime. It supports property access control, value comparison, and cloning capabilities.
  13. /// </remarks>
  14. public class Dyna : IDyna
  15. {
  16. #region *** Private Fields ***
  17. /// <summary>
  18. /// Thread-safe dictionary storing the dynamic properties and their values.
  19. /// </summary>
  20. private IDictionary<string, DynaPropertyWithValue> _properties = new ConcurrentDictionary<string, DynaPropertyWithValue>();
  21. private IList<string> _changedPropertes = new List<string>();
  22. private IList<Tuple<string, object?>> _defaultValues = new List<Tuple<string, object?>>();
  23. #endregion
  24. #region *** Properties ***
  25. /// <summary>
  26. /// Gets an array of all property definitions contained in this Dyna instance.
  27. /// </summary>
  28. /// <returns>An array of <see cref="IDynaProperty"/> objects representing the properties.</returns>
  29. public IDynaProperty[] Properties => _properties.Values.Select(x => x.Property).ToArray();
  30. /// <summary>
  31. /// Gets an array of all property names changed after calling TrackChanges.
  32. /// </summary>
  33. /// <returns>An array of <see cref="string"/> contains property names.</returns>
  34. public string[] ChangedProperties => _changedPropertes.ToArray();
  35. /// <summary>
  36. /// Gets or sets the value of a property by its name.
  37. /// </summary>
  38. /// <param name="propertyName">The name of the property to access.</param>
  39. /// <returns>The value of the specified property.</returns>
  40. /// <exception cref="ArgumentOutOfRangeException">Thrown when the property does not exist.</exception>
  41. public object? this[string propertyName]
  42. {
  43. get => GetValue<object>(propertyName);
  44. set => SetValue(propertyName, value);
  45. }
  46. #endregion
  47. #region *** Constructors ***
  48. /// <summary>
  49. /// Initializes a new instance of the <see cref="Dyna"/> class with no properties.
  50. /// </summary>
  51. public Dyna()
  52. {
  53. TrackChanges();
  54. }
  55. /// <summary>
  56. /// Initializes a new instance of the <see cref="Dyna"/> class with properties derived from the specified type.
  57. /// </summary>
  58. /// <param name="templateType">The type whose properties should be reflected into this Dyna instance.</param>
  59. /// <exception cref="ArgumentException">Thrown when the template type does not have a valid FullName.</exception>
  60. public Dyna(Type templateType)
  61. {
  62. AnalyzeType(templateType);
  63. TrackChanges();
  64. }
  65. /// <summary>
  66. /// Initializes a new instance of the <see cref="Dyna"/> class with the specified properties.
  67. /// </summary>
  68. /// <param name="properties">A collection of <see cref="DynaProperty"/> objects defining the properties for this instance.</param>
  69. public Dyna(IEnumerable<DynaProperty> properties) : this()
  70. {
  71. AnalyzeProperties(properties);
  72. TrackChanges();
  73. }
  74. #endregion
  75. #region *** Public Operations ***
  76. /// <summary>
  77. /// Gets the value of a property as a specified type.
  78. /// </summary>
  79. /// <typeparam name="TValue">The type to convert the property value to.</typeparam>
  80. /// <param name="propertyName">The name of the property to get the value from.</param>
  81. /// <returns>The value of the property cast to the specified type, or null if the property value is null.</returns>
  82. /// <exception cref="ArgumentOutOfRangeException">Thrown when the property does not exist.</exception>
  83. public TValue? GetValue<TValue>(string propertyName)
  84. {
  85. if (_properties.TryGetValue(propertyName, out var property))
  86. {
  87. if (property.Value == null) return default;
  88. return (TValue)property.Value;
  89. }
  90. throw new ArgumentOutOfRangeException(nameof(propertyName), $"Property '{propertyName}' not found in Dyna collection.");
  91. }
  92. /// <summary>
  93. /// Sets the value of a property.
  94. /// </summary>
  95. /// <typeparam name="TValue">The type of the value being set.</typeparam>
  96. /// <param name="propertyName">The name of the property to set.</param>
  97. /// <param name="value">The value to set the property to.</param>
  98. /// <exception cref="ArgumentOutOfRangeException">Thrown when the property does not exist.</exception>
  99. public void SetValue<TValue>(string propertyName, TValue? value)
  100. {
  101. if (_properties.TryGetValue(propertyName, out var property))
  102. {
  103. if (!object.Equals(property.Value, value))
  104. {
  105. // check if new value is the same as the default value, then discard the change
  106. if (object.Equals(value, _defaultValues.First(x=>x.Item1 == propertyName).Item2))
  107. {
  108. property.Value = value;
  109. _changedPropertes.Remove(propertyName);
  110. return;
  111. }
  112. if (OnChangingProperty(propertyName, property.Value, value))
  113. {
  114. property.Value = value;
  115. // track changes
  116. if (!_changedPropertes.Contains(propertyName))
  117. _changedPropertes.Add(propertyName);
  118. }
  119. }
  120. }
  121. else
  122. throw new ArgumentOutOfRangeException(nameof(propertyName), $"Property '{propertyName}' not found in Dyna collection.");
  123. }
  124. /// <summary>
  125. /// Clears all property values, setting them to null.
  126. /// </summary>
  127. public void Clear()
  128. {
  129. foreach (var property in _properties)
  130. property.Value.Value = null;
  131. OnClear();
  132. TrackChanges();
  133. }
  134. /// <summary>
  135. /// Tracks all property changes after calling this method.
  136. /// </summary>
  137. public void TrackChanges()
  138. {
  139. _changedPropertes.Clear();
  140. _defaultValues.Clear();
  141. // create new snapshot of default values
  142. foreach (var property in _properties)
  143. _defaultValues.Add(new Tuple<string, object?>(property.Key, property.Value.Value));
  144. }
  145. /// <summary>
  146. /// Determines whether this Dyna instance has the same property structure as another Dyna instance.
  147. /// </summary>
  148. /// <param name="target">The Dyna instance to compare with.</param>
  149. /// <returns>
  150. /// <c>true</c> if both instances have the same property names and types; otherwise, <c>false</c>.
  151. /// </returns>
  152. public bool IsSame(Dyna target)
  153. {
  154. if (_properties.Count != target._properties.Count)
  155. return false;
  156. foreach (var property in _properties)
  157. {
  158. if (!target._properties.TryGetValue(property.Key, out var targetProperty))
  159. return false;
  160. }
  161. return true;
  162. }
  163. /// <summary>
  164. /// Determines whether this Dyna instance has the same properties and values as another Dyna instance.
  165. /// </summary>
  166. /// <param name="target">The Dyna instance to compare with.</param>
  167. /// <returns>
  168. /// <c>true</c> if both instances have the same properties and values; otherwise, <c>false</c>.
  169. /// </returns>
  170. public bool IsEquals(Dyna target)
  171. {
  172. if (_properties.Count != target._properties.Count)
  173. return false;
  174. foreach (var property in _properties)
  175. {
  176. if (!target._properties.TryGetValue(property.Key, out var targetProperty))
  177. return false;
  178. if (!Equals(property.Value.Value, targetProperty.Value))
  179. return false;
  180. }
  181. return true;
  182. }
  183. /// <summary>
  184. /// Determines whether this Dyna instance contains a property with the specified name.
  185. /// </summary>
  186. /// <param name="propertyName">The name of the property to check for.</param>
  187. /// <returns><c>true</c> if the property exists; otherwise, <c>false</c>.</returns>
  188. public bool ContainsProperty(string propertyName)
  189. {
  190. return _properties.ContainsKey(propertyName);
  191. }
  192. /// <summary>
  193. /// Copies all property values from this Dyna instance to a target Dyna instance.
  194. /// </summary>
  195. /// <param name="target">The target Dyna instance to copy values to.</param>
  196. /// <remarks>
  197. /// This method only copies values for properties that exist in both instances.
  198. /// It does not create new properties in the target instance.
  199. /// </remarks>
  200. public void CopyTo(IDyna target)
  201. {
  202. foreach (var property in _properties)
  203. target.SetValue(property.Key, property.Value.Value);
  204. }
  205. /// <summary>
  206. /// Creates a deep clone of this Dyna instance.
  207. /// </summary>
  208. /// <returns>A new Dyna instance with the same properties and values.</returns>
  209. public object Clone()
  210. {
  211. var clone = new Dyna(_properties.Values.Select(x => x.Property));
  212. foreach (var property in _properties)
  213. clone.SetValue(property.Key, property.Value.Value);
  214. clone.TrackChanges();
  215. return clone;
  216. }
  217. #endregion
  218. #region *** Virtual Operations ***
  219. public virtual bool OnChangingProperty(string propertyName, object? oldValue, object? newValue)
  220. {
  221. return true;
  222. }
  223. public virtual void OnClear()
  224. {
  225. }
  226. #endregion
  227. #region *** Private Operations ***
  228. /// <summary>
  229. /// Analyzes a type using reflection and creates corresponding dynamic properties.
  230. /// </summary>
  231. /// <param name="templateType">The type to analyze.</param>
  232. /// <exception cref="ArgumentException">Thrown when the template type does not have a valid FullName.</exception>
  233. private void AnalyzeType(Type templateType)
  234. {
  235. if (templateType.FullName == null)
  236. throw new ArgumentException("Type must have a FullName", nameof(templateType));
  237. foreach (var property in templateType.GetProperties())
  238. _properties.Add(property.Name, new DynaPropertyWithValue(new DynaProperty(property.Name, property.PropertyType, DynaPropertyAccessability.Public, DynaPropertyAccessability.Public), null));
  239. }
  240. /// <summary>
  241. /// Creates dynamic properties from a collection of property definitions.
  242. /// </summary>
  243. /// <param name="properties">The collection of property definitions to use.</param>
  244. private void AnalyzeProperties(IEnumerable<DynaProperty> properties)
  245. {
  246. foreach (var property in properties)
  247. _properties.Add(property.Name, new DynaPropertyWithValue(property, null));
  248. }
  249. #endregion
  250. #region *** Nested Classes ***
  251. /// <summary>
  252. /// Represents a dynamic property with its metadata.
  253. /// </summary>
  254. public sealed class DynaProperty : IDynaProperty
  255. {
  256. #region *** Properties ***
  257. /// <summary>
  258. /// Gets the name of the property.
  259. /// </summary>
  260. public string Name { get; private set; }
  261. /// <summary>
  262. /// Gets the type of the property.
  263. /// </summary>
  264. public Type ValueType { get; private set; }
  265. /// <summary>
  266. /// Gets the accessibility level for reading the property.
  267. /// </summary>
  268. public DynaPropertyAccessability GetAccessability { get; private set; }
  269. /// <summary>
  270. /// Gets the accessibility level for writing to the property.
  271. /// </summary>
  272. public DynaPropertyAccessability SetAccessability { get; private set; }
  273. #endregion
  274. #region *** Constructors ***
  275. /// <summary>
  276. /// Initializes a new instance of the <see cref="DynaProperty"/> class.
  277. /// </summary>
  278. /// <param name="name">The name of the property.</param>
  279. /// <param name="valueType">The type of the property.</param>
  280. /// <param name="getAccessability">The accessibility level for reading the property.</param>
  281. /// <param name="setAccessability">The accessibility level for writing to the property.</param>
  282. public DynaProperty(string name, Type valueType, DynaPropertyAccessability getAccessability, DynaPropertyAccessability setAccessability)
  283. {
  284. Name = name;
  285. ValueType = valueType;
  286. GetAccessability = getAccessability;
  287. SetAccessability = setAccessability;
  288. }
  289. #endregion
  290. }
  291. /// <summary>
  292. /// Internal class that pairs a property definition with its value.
  293. /// </summary>
  294. private sealed class DynaPropertyWithValue
  295. {
  296. /// <summary>
  297. /// Gets the property definition.
  298. /// </summary>
  299. public DynaProperty Property { get; private set; }
  300. /// <summary>
  301. /// Gets or sets the current value of the property.
  302. /// </summary>
  303. public object? Value { get; set; }
  304. /// <summary>
  305. /// Initializes a new instance of the <see cref="DynaPropertyWithValue"/> class.
  306. /// </summary>
  307. /// <param name="property">The property definition.</param>
  308. /// <param name="value">The initial value of the property.</param>
  309. public DynaPropertyWithValue(DynaProperty property, object? value)
  310. {
  311. Property = property;
  312. Value = value;
  313. }
  314. }
  315. #endregion
  316. }
  317. }