| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357 |
- 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
- }
- }
|