using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
namespace Quadarax.Foundation.Core.Object.Dynamic
{
///
/// Provides a dynamic object implementation that allows for runtime definition and manipulation of properties.
///
///
/// 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.
///
public class Dyna : IDyna
{
#region *** Private Fields ***
///
/// Thread-safe dictionary storing the dynamic properties and their values.
///
private IDictionary _properties = new ConcurrentDictionary();
private IList _changedPropertes = new List();
private IList> _defaultValues = new List>();
#endregion
#region *** Properties ***
///
/// Gets an array of all property definitions contained in this Dyna instance.
///
/// An array of objects representing the properties.
public IDynaProperty[] Properties => _properties.Values.Select(x => x.Property).ToArray();
///
/// Gets an array of all property names changed after calling TrackChanges.
///
/// An array of contains property names.
public string[] ChangedProperties => _changedPropertes.ToArray();
///
/// Gets or sets the value of a property by its name.
///
/// The name of the property to access.
/// The value of the specified property.
/// Thrown when the property does not exist.
public object? this[string propertyName]
{
get => GetValue(propertyName);
set => SetValue(propertyName, value);
}
#endregion
#region *** Constructors ***
///
/// Initializes a new instance of the class with no properties.
///
public Dyna()
{
TrackChanges();
}
///
/// Initializes a new instance of the class with properties derived from the specified type.
///
/// The type whose properties should be reflected into this Dyna instance.
/// Thrown when the template type does not have a valid FullName.
public Dyna(Type templateType)
{
AnalyzeType(templateType);
TrackChanges();
}
///
/// Initializes a new instance of the class with the specified properties.
///
/// A collection of objects defining the properties for this instance.
public Dyna(IEnumerable properties) : this()
{
AnalyzeProperties(properties);
TrackChanges();
}
#endregion
#region *** Public Operations ***
///
/// Gets the value of a property as a specified type.
///
/// The type to convert the property value to.
/// The name of the property to get the value from.
/// The value of the property cast to the specified type, or null if the property value is null.
/// Thrown when the property does not exist.
public TValue? GetValue(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.");
}
///
/// Sets the value of a property.
///
/// The type of the value being set.
/// The name of the property to set.
/// The value to set the property to.
/// Thrown when the property does not exist.
public void SetValue(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.");
}
///
/// Clears all property values, setting them to null.
///
public void Clear()
{
foreach (var property in _properties)
property.Value.Value = null;
OnClear();
TrackChanges();
}
///
/// Tracks all property changes after calling this method.
///
public void TrackChanges()
{
_changedPropertes.Clear();
_defaultValues.Clear();
// create new snapshot of default values
foreach (var property in _properties)
_defaultValues.Add(new Tuple(property.Key, property.Value.Value));
}
///
/// Determines whether this Dyna instance has the same property structure as another Dyna instance.
///
/// The Dyna instance to compare with.
///
/// true if both instances have the same property names and types; otherwise, false .
///
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;
}
///
/// Determines whether this Dyna instance has the same properties and values as another Dyna instance.
///
/// The Dyna instance to compare with.
///
/// true if both instances have the same properties and values; otherwise, false .
///
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;
}
///
/// Determines whether this Dyna instance contains a property with the specified name.
///
/// The name of the property to check for.
/// true if the property exists; otherwise, false .
public bool ContainsProperty(string propertyName)
{
return _properties.ContainsKey(propertyName);
}
///
/// Copies all property values from this Dyna instance to a target Dyna instance.
///
/// The target Dyna instance to copy values to.
///
/// This method only copies values for properties that exist in both instances.
/// It does not create new properties in the target instance.
///
public void CopyTo(IDyna target)
{
foreach (var property in _properties)
target.SetValue(property.Key, property.Value.Value);
}
///
/// Creates a deep clone of this Dyna instance.
///
/// A new Dyna instance with the same properties and values.
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 ***
///
/// Analyzes a type using reflection and creates corresponding dynamic properties.
///
/// The type to analyze.
/// Thrown when the template type does not have a valid FullName.
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));
}
///
/// Creates dynamic properties from a collection of property definitions.
///
/// The collection of property definitions to use.
private void AnalyzeProperties(IEnumerable properties)
{
foreach (var property in properties)
_properties.Add(property.Name, new DynaPropertyWithValue(property, null));
}
#endregion
#region *** Nested Classes ***
///
/// Represents a dynamic property with its metadata.
///
public sealed class DynaProperty : IDynaProperty
{
#region *** Properties ***
///
/// Gets the name of the property.
///
public string Name { get; private set; }
///
/// Gets the type of the property.
///
public Type ValueType { get; private set; }
///
/// Gets the accessibility level for reading the property.
///
public DynaPropertyAccessability GetAccessability { get; private set; }
///
/// Gets the accessibility level for writing to the property.
///
public DynaPropertyAccessability SetAccessability { get; private set; }
#endregion
#region *** Constructors ***
///
/// Initializes a new instance of the class.
///
/// The name of the property.
/// The type of the property.
/// The accessibility level for reading the property.
/// The accessibility level for writing to the property.
public DynaProperty(string name, Type valueType, DynaPropertyAccessability getAccessability, DynaPropertyAccessability setAccessability)
{
Name = name;
ValueType = valueType;
GetAccessability = getAccessability;
SetAccessability = setAccessability;
}
#endregion
}
///
/// Internal class that pairs a property definition with its value.
///
private sealed class DynaPropertyWithValue
{
///
/// Gets the property definition.
///
public DynaProperty Property { get; private set; }
///
/// Gets or sets the current value of the property.
///
public object? Value { get; set; }
///
/// Initializes a new instance of the class.
///
/// The property definition.
/// The initial value of the property.
public DynaPropertyWithValue(DynaProperty property, object? value)
{
Property = property;
Value = value;
}
}
#endregion
}
}