|
@@ -0,0 +1,357 @@
|
|
|
|
|
+using System;
|
|
|
|
|
+using System.Collections.Concurrent;
|
|
|
|
|
+using System.Collections.Generic;
|
|
|
|
|
+using System.Linq;
|
|
|
|
|
+
|
|
|
|
|
+namespace Quadarax.Foundation.Core.Object.Dynamic
|
|
|
|
|
+{
|
|
|
|
|
+ /// <summary>
|
|
|
|
|
+ /// Provides a dynamic object implementation that allows for runtime definition and manipulation of properties.
|
|
|
|
|
+ /// </summary>
|
|
|
|
|
+ /// <remarks>
|
|
|
|
|
+ /// The Dyna class offers a flexible way to create objects with dynamic property sets that can be
|
|
|
|
|
+ /// defined at runtime. It supports property access control, value comparison, and cloning capabilities.
|
|
|
|
|
+ /// </remarks>
|
|
|
|
|
+ public class Dyna : IDyna
|
|
|
|
|
+ {
|
|
|
|
|
+ #region *** Private Fields ***
|
|
|
|
|
+ /// <summary>
|
|
|
|
|
+ /// Thread-safe dictionary storing the dynamic properties and their values.
|
|
|
|
|
+ /// </summary>
|
|
|
|
|
+ private IDictionary<string, DynaPropertyWithValue> _properties = new ConcurrentDictionary<string, DynaPropertyWithValue>();
|
|
|
|
|
+
|
|
|
|
|
+ private IList<string> _changedPropertes = new List<string>();
|
|
|
|
|
+ private IList<Tuple<string, object?>> _defaultValues = new List<Tuple<string, object?>>();
|
|
|
|
|
+ #endregion
|
|
|
|
|
+
|
|
|
|
|
+ #region *** Properties ***
|
|
|
|
|
+ /// <summary>
|
|
|
|
|
+ /// Gets an array of all property definitions contained in this Dyna instance.
|
|
|
|
|
+ /// </summary>
|
|
|
|
|
+ /// <returns>An array of <see cref="IDynaProperty"/> objects representing the properties.</returns>
|
|
|
|
|
+ public IDynaProperty[] Properties => _properties.Values.Select(x => x.Property).ToArray();
|
|
|
|
|
+
|
|
|
|
|
+ /// <summary>
|
|
|
|
|
+ /// Gets an array of all property names changed after calling TrackChanges.
|
|
|
|
|
+ /// </summary>
|
|
|
|
|
+ /// <returns>An array of <see cref="string"/> contains property names.</returns>
|
|
|
|
|
+ public string[] ChangedProperties => _changedPropertes.ToArray();
|
|
|
|
|
+
|
|
|
|
|
+ /// <summary>
|
|
|
|
|
+ /// Gets or sets the value of a property by its name.
|
|
|
|
|
+ /// </summary>
|
|
|
|
|
+ /// <param name="propertyName">The name of the property to access.</param>
|
|
|
|
|
+ /// <returns>The value of the specified property.</returns>
|
|
|
|
|
+ /// <exception cref="ArgumentOutOfRangeException">Thrown when the property does not exist.</exception>
|
|
|
|
|
+ public object? this[string propertyName]
|
|
|
|
|
+ {
|
|
|
|
|
+ get => GetValue<object>(propertyName);
|
|
|
|
|
+ set => SetValue(propertyName, value);
|
|
|
|
|
+ }
|
|
|
|
|
+ #endregion
|
|
|
|
|
+
|
|
|
|
|
+ #region *** Constructors ***
|
|
|
|
|
+ /// <summary>
|
|
|
|
|
+ /// Initializes a new instance of the <see cref="Dyna"/> class with no properties.
|
|
|
|
|
+ /// </summary>
|
|
|
|
|
+ public Dyna()
|
|
|
|
|
+ {
|
|
|
|
|
+ TrackChanges();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /// <summary>
|
|
|
|
|
+ /// Initializes a new instance of the <see cref="Dyna"/> class with properties derived from the specified type.
|
|
|
|
|
+ /// </summary>
|
|
|
|
|
+ /// <param name="templateType">The type whose properties should be reflected into this Dyna instance.</param>
|
|
|
|
|
+ /// <exception cref="ArgumentException">Thrown when the template type does not have a valid FullName.</exception>
|
|
|
|
|
+ public Dyna(Type templateType)
|
|
|
|
|
+ {
|
|
|
|
|
+ AnalyzeType(templateType);
|
|
|
|
|
+ TrackChanges();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /// <summary>
|
|
|
|
|
+ /// Initializes a new instance of the <see cref="Dyna"/> class with the specified properties.
|
|
|
|
|
+ /// </summary>
|
|
|
|
|
+ /// <param name="properties">A collection of <see cref="DynaProperty"/> objects defining the properties for this instance.</param>
|
|
|
|
|
+ public Dyna(IEnumerable<DynaProperty> properties) : this()
|
|
|
|
|
+ {
|
|
|
|
|
+ AnalyzeProperties(properties);
|
|
|
|
|
+ TrackChanges();
|
|
|
|
|
+ }
|
|
|
|
|
+ #endregion
|
|
|
|
|
+
|
|
|
|
|
+ #region *** Public Operations ***
|
|
|
|
|
+ /// <summary>
|
|
|
|
|
+ /// Gets the value of a property as a specified type.
|
|
|
|
|
+ /// </summary>
|
|
|
|
|
+ /// <typeparam name="TValue">The type to convert the property value to.</typeparam>
|
|
|
|
|
+ /// <param name="propertyName">The name of the property to get the value from.</param>
|
|
|
|
|
+ /// <returns>The value of the property cast to the specified type, or null if the property value is null.</returns>
|
|
|
|
|
+ /// <exception cref="ArgumentOutOfRangeException">Thrown when the property does not exist.</exception>
|
|
|
|
|
+ public TValue? GetValue<TValue>(string propertyName)
|
|
|
|
|
+ {
|
|
|
|
|
+ if (_properties.TryGetValue(propertyName, out var property))
|
|
|
|
|
+ {
|
|
|
|
|
+ if (property.Value == null) return default;
|
|
|
|
|
+ return (TValue)property.Value;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ throw new ArgumentOutOfRangeException(nameof(propertyName), $"Property '{propertyName}' not found in Dyna collection.");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /// <summary>
|
|
|
|
|
+ /// Sets the value of a property.
|
|
|
|
|
+ /// </summary>
|
|
|
|
|
+ /// <typeparam name="TValue">The type of the value being set.</typeparam>
|
|
|
|
|
+ /// <param name="propertyName">The name of the property to set.</param>
|
|
|
|
|
+ /// <param name="value">The value to set the property to.</param>
|
|
|
|
|
+ /// <exception cref="ArgumentOutOfRangeException">Thrown when the property does not exist.</exception>
|
|
|
|
|
+ public void SetValue<TValue>(string propertyName, TValue? value)
|
|
|
|
|
+ {
|
|
|
|
|
+ if (_properties.TryGetValue(propertyName, out var property))
|
|
|
|
|
+ {
|
|
|
|
|
+ if (!object.Equals(property.Value, value))
|
|
|
|
|
+ {
|
|
|
|
|
+ // check if new value is the same as the default value, then discard the change
|
|
|
|
|
+ if (object.Equals(value, _defaultValues.First(x=>x.Item1 == propertyName).Item2))
|
|
|
|
|
+ {
|
|
|
|
|
+ property.Value = value;
|
|
|
|
|
+ _changedPropertes.Remove(propertyName);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (OnChangingProperty(propertyName, property.Value, value))
|
|
|
|
|
+ {
|
|
|
|
|
+ property.Value = value;
|
|
|
|
|
+
|
|
|
|
|
+ // track changes
|
|
|
|
|
+ if (!_changedPropertes.Contains(propertyName))
|
|
|
|
|
+ _changedPropertes.Add(propertyName);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ else
|
|
|
|
|
+ throw new ArgumentOutOfRangeException(nameof(propertyName), $"Property '{propertyName}' not found in Dyna collection.");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /// <summary>
|
|
|
|
|
+ /// Clears all property values, setting them to null.
|
|
|
|
|
+ /// </summary>
|
|
|
|
|
+ public void Clear()
|
|
|
|
|
+ {
|
|
|
|
|
+ foreach (var property in _properties)
|
|
|
|
|
+ property.Value.Value = null;
|
|
|
|
|
+
|
|
|
|
|
+ OnClear();
|
|
|
|
|
+
|
|
|
|
|
+ TrackChanges();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /// <summary>
|
|
|
|
|
+ /// Tracks all property changes after calling this method.
|
|
|
|
|
+ /// </summary>
|
|
|
|
|
+ public void TrackChanges()
|
|
|
|
|
+ {
|
|
|
|
|
+ _changedPropertes.Clear();
|
|
|
|
|
+ _defaultValues.Clear();
|
|
|
|
|
+
|
|
|
|
|
+ // create new snapshot of default values
|
|
|
|
|
+ foreach (var property in _properties)
|
|
|
|
|
+ _defaultValues.Add(new Tuple<string, object?>(property.Key, property.Value.Value));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /// <summary>
|
|
|
|
|
+ /// Determines whether this Dyna instance has the same property structure as another Dyna instance.
|
|
|
|
|
+ /// </summary>
|
|
|
|
|
+ /// <param name="target">The Dyna instance to compare with.</param>
|
|
|
|
|
+ /// <returns>
|
|
|
|
|
+ /// <c>true</c> if both instances have the same property names and types; otherwise, <c>false</c>.
|
|
|
|
|
+ /// </returns>
|
|
|
|
|
+ public bool IsSame(Dyna target)
|
|
|
|
|
+ {
|
|
|
|
|
+ if (_properties.Count != target._properties.Count)
|
|
|
|
|
+ return false;
|
|
|
|
|
+ foreach (var property in _properties)
|
|
|
|
|
+ {
|
|
|
|
|
+ if (!target._properties.TryGetValue(property.Key, out var targetProperty))
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ return true;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /// <summary>
|
|
|
|
|
+ /// Determines whether this Dyna instance has the same properties and values as another Dyna instance.
|
|
|
|
|
+ /// </summary>
|
|
|
|
|
+ /// <param name="target">The Dyna instance to compare with.</param>
|
|
|
|
|
+ /// <returns>
|
|
|
|
|
+ /// <c>true</c> if both instances have the same properties and values; otherwise, <c>false</c>.
|
|
|
|
|
+ /// </returns>
|
|
|
|
|
+ public bool IsEquals(Dyna target)
|
|
|
|
|
+ {
|
|
|
|
|
+ if (_properties.Count != target._properties.Count)
|
|
|
|
|
+ return false;
|
|
|
|
|
+ foreach (var property in _properties)
|
|
|
|
|
+ {
|
|
|
|
|
+ if (!target._properties.TryGetValue(property.Key, out var targetProperty))
|
|
|
|
|
+ return false;
|
|
|
|
|
+ if (!Equals(property.Value.Value, targetProperty.Value))
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ return true;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /// <summary>
|
|
|
|
|
+ /// Determines whether this Dyna instance contains a property with the specified name.
|
|
|
|
|
+ /// </summary>
|
|
|
|
|
+ /// <param name="propertyName">The name of the property to check for.</param>
|
|
|
|
|
+ /// <returns><c>true</c> if the property exists; otherwise, <c>false</c>.</returns>
|
|
|
|
|
+ public bool ContainsProperty(string propertyName)
|
|
|
|
|
+ {
|
|
|
|
|
+ return _properties.ContainsKey(propertyName);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /// <summary>
|
|
|
|
|
+ /// Copies all property values from this Dyna instance to a target Dyna instance.
|
|
|
|
|
+ /// </summary>
|
|
|
|
|
+ /// <param name="target">The target Dyna instance to copy values to.</param>
|
|
|
|
|
+ /// <remarks>
|
|
|
|
|
+ /// This method only copies values for properties that exist in both instances.
|
|
|
|
|
+ /// It does not create new properties in the target instance.
|
|
|
|
|
+ /// </remarks>
|
|
|
|
|
+ public void CopyTo(IDyna target)
|
|
|
|
|
+ {
|
|
|
|
|
+ foreach (var property in _properties)
|
|
|
|
|
+ target.SetValue(property.Key, property.Value.Value);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /// <summary>
|
|
|
|
|
+ /// Creates a deep clone of this Dyna instance.
|
|
|
|
|
+ /// </summary>
|
|
|
|
|
+ /// <returns>A new Dyna instance with the same properties and values.</returns>
|
|
|
|
|
+ public object Clone()
|
|
|
|
|
+ {
|
|
|
|
|
+ var clone = new Dyna(_properties.Values.Select(x => x.Property));
|
|
|
|
|
+ foreach (var property in _properties)
|
|
|
|
|
+ clone.SetValue(property.Key, property.Value.Value);
|
|
|
|
|
+
|
|
|
|
|
+ clone.TrackChanges();
|
|
|
|
|
+ return clone;
|
|
|
|
|
+ }
|
|
|
|
|
+ #endregion
|
|
|
|
|
+
|
|
|
|
|
+ #region *** Virtual Operations ***
|
|
|
|
|
+ public virtual bool OnChangingProperty(string propertyName, object? oldValue, object? newValue)
|
|
|
|
|
+ {
|
|
|
|
|
+ return true;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public virtual void OnClear()
|
|
|
|
|
+ {
|
|
|
|
|
+
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ #endregion
|
|
|
|
|
+
|
|
|
|
|
+ #region *** Private Operations ***
|
|
|
|
|
+ /// <summary>
|
|
|
|
|
+ /// Analyzes a type using reflection and creates corresponding dynamic properties.
|
|
|
|
|
+ /// </summary>
|
|
|
|
|
+ /// <param name="templateType">The type to analyze.</param>
|
|
|
|
|
+ /// <exception cref="ArgumentException">Thrown when the template type does not have a valid FullName.</exception>
|
|
|
|
|
+ private void AnalyzeType(Type templateType)
|
|
|
|
|
+ {
|
|
|
|
|
+ if (templateType.FullName == null)
|
|
|
|
|
+ throw new ArgumentException("Type must have a FullName", nameof(templateType));
|
|
|
|
|
+
|
|
|
|
|
+ foreach (var property in templateType.GetProperties())
|
|
|
|
|
+ _properties.Add(property.Name, new DynaPropertyWithValue(new DynaProperty(property.Name, property.PropertyType, DynaPropertyAccessability.Public, DynaPropertyAccessability.Public), null));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /// <summary>
|
|
|
|
|
+ /// Creates dynamic properties from a collection of property definitions.
|
|
|
|
|
+ /// </summary>
|
|
|
|
|
+ /// <param name="properties">The collection of property definitions to use.</param>
|
|
|
|
|
+ private void AnalyzeProperties(IEnumerable<DynaProperty> properties)
|
|
|
|
|
+ {
|
|
|
|
|
+ foreach (var property in properties)
|
|
|
|
|
+ _properties.Add(property.Name, new DynaPropertyWithValue(property, null));
|
|
|
|
|
+ }
|
|
|
|
|
+ #endregion
|
|
|
|
|
+
|
|
|
|
|
+ #region *** Nested Classes ***
|
|
|
|
|
+ /// <summary>
|
|
|
|
|
+ /// Represents a dynamic property with its metadata.
|
|
|
|
|
+ /// </summary>
|
|
|
|
|
+ public sealed class DynaProperty : IDynaProperty
|
|
|
|
|
+ {
|
|
|
|
|
+ #region *** Properties ***
|
|
|
|
|
+ /// <summary>
|
|
|
|
|
+ /// Gets the name of the property.
|
|
|
|
|
+ /// </summary>
|
|
|
|
|
+ public string Name { get; private set; }
|
|
|
|
|
+
|
|
|
|
|
+ /// <summary>
|
|
|
|
|
+ /// Gets the type of the property.
|
|
|
|
|
+ /// </summary>
|
|
|
|
|
+ public Type ValueType { get; private set; }
|
|
|
|
|
+
|
|
|
|
|
+ /// <summary>
|
|
|
|
|
+ /// Gets the accessibility level for reading the property.
|
|
|
|
|
+ /// </summary>
|
|
|
|
|
+ public DynaPropertyAccessability GetAccessability { get; private set; }
|
|
|
|
|
+
|
|
|
|
|
+ /// <summary>
|
|
|
|
|
+ /// Gets the accessibility level for writing to the property.
|
|
|
|
|
+ /// </summary>
|
|
|
|
|
+ public DynaPropertyAccessability SetAccessability { get; private set; }
|
|
|
|
|
+
|
|
|
|
|
+ #endregion
|
|
|
|
|
+
|
|
|
|
|
+ #region *** Constructors ***
|
|
|
|
|
+ /// <summary>
|
|
|
|
|
+ /// Initializes a new instance of the <see cref="DynaProperty"/> class.
|
|
|
|
|
+ /// </summary>
|
|
|
|
|
+ /// <param name="name">The name of the property.</param>
|
|
|
|
|
+ /// <param name="valueType">The type of the property.</param>
|
|
|
|
|
+ /// <param name="getAccessability">The accessibility level for reading the property.</param>
|
|
|
|
|
+ /// <param name="setAccessability">The accessibility level for writing to the property.</param>
|
|
|
|
|
+ public DynaProperty(string name, Type valueType, DynaPropertyAccessability getAccessability, DynaPropertyAccessability setAccessability)
|
|
|
|
|
+ {
|
|
|
|
|
+ Name = name;
|
|
|
|
|
+ ValueType = valueType;
|
|
|
|
|
+ GetAccessability = getAccessability;
|
|
|
|
|
+ SetAccessability = setAccessability;
|
|
|
|
|
+ }
|
|
|
|
|
+ #endregion
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /// <summary>
|
|
|
|
|
+ /// Internal class that pairs a property definition with its value.
|
|
|
|
|
+ /// </summary>
|
|
|
|
|
+ private sealed class DynaPropertyWithValue
|
|
|
|
|
+ {
|
|
|
|
|
+ /// <summary>
|
|
|
|
|
+ /// Gets the property definition.
|
|
|
|
|
+ /// </summary>
|
|
|
|
|
+ public DynaProperty Property { get; private set; }
|
|
|
|
|
+
|
|
|
|
|
+ /// <summary>
|
|
|
|
|
+ /// Gets or sets the current value of the property.
|
|
|
|
|
+ /// </summary>
|
|
|
|
|
+ public object? Value { get; set; }
|
|
|
|
|
+
|
|
|
|
|
+ /// <summary>
|
|
|
|
|
+ /// Initializes a new instance of the <see cref="DynaPropertyWithValue"/> class.
|
|
|
|
|
+ /// </summary>
|
|
|
|
|
+ /// <param name="property">The property definition.</param>
|
|
|
|
|
+ /// <param name="value">The initial value of the property.</param>
|
|
|
|
|
+ public DynaPropertyWithValue(DynaProperty property, object? value)
|
|
|
|
|
+ {
|
|
|
|
|
+ Property = property;
|
|
|
|
|
+ Value = value;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ #endregion
|
|
|
|
|
+ }
|
|
|
|
|
+}
|