using AutoProperties; using Newtonsoft.Json; using System; using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Linq.Expressions; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Serialization; namespace InABox.Core { public class DoNotSerialize : Attribute { } public interface IBaseObject { public bool IsChanged(); public void CancelChanges(); public void CommitChanges(); public bool IsObserving(); public void SetObserving(bool active); } public interface IOriginalValues : IEnumerable> { public object? this[string key] { get; set; } public bool ContainsKey(string key); public void Clear(); public bool TryGetValue(string key, out object? value); public bool TryAdd(string key, object? value); public void Remove(string key); public object? GetValueOrDefault(string key) { if(TryGetValue(key, out object? value)) return value; return null; } } public class OriginalValues : IOriginalValues { public OriginalValues() { } public ConcurrentDictionary Dictionary { get; set; } = new ConcurrentDictionary(); public object? this[string key] { get => Dictionary[key]; set => Dictionary[key] = value; } public void Clear() { Dictionary.Clear(); } public bool ContainsKey(string key) => Dictionary.ContainsKey(key); public IEnumerator> GetEnumerator() { return Dictionary.GetEnumerator(); } public bool TryGetValue(string key, out object? value) { return Dictionary.TryGetValue(key, out value); } public bool TryAdd(string key, object? value) { return Dictionary.TryAdd(key, value); } public void Remove(string key) { (Dictionary as IDictionary).Remove(key); } IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } public interface ILoadedColumns : IEnumerable { public bool Add(string key); public bool Contains(string key); } public class LoadedColumns : ILoadedColumns { private HashSet Columns = new HashSet(); public bool Add(string key) { return Columns.Add(key); } public bool Contains(string key) { return Columns.Contains(key); } public IEnumerator GetEnumerator() { return Columns.GetEnumerator(); } IEnumerator IEnumerable.GetEnumerator() { return Columns.GetEnumerator(); } } /// /// Observable object with INotifyPropertyChanged implemented /// public abstract class BaseObject : INotifyPropertyChanged, IBaseObject { public BaseObject() { SetObserving(false); Init(); SetObserving(true); } internal bool _disabledInterceptor; protected T InitializeField(ref T? field, [CallerMemberName] string name = "") where T : BaseObject, ISubObject, new() { if(field is null) { var value = new T(); value.SetLinkedParent(this); value.SetLinkedPath(name); value.SetObserving(_observing); field = value; } return field; } [GetInterceptor] protected T GetValue(Type propertyType, ref T field, string name) { if (_disabledInterceptor) return field; if(field is null && propertyType.HasInterface() && !propertyType.IsAbstract) { var value = Activator.CreateInstance(); var subObj = (value as ISubObject)!; subObj.SetLinkedParent(this); subObj.SetLinkedPath(name); if(subObj is BaseObject obj) { obj.SetObserving(_observing); } field = value; } return field; } [SetInterceptor] protected void SetValue(ref T field, T newValue) { field = newValue; } [OnDeserializing] internal void OnDeserializingMethod(StreamingContext context) { if (_observing) SetObserving(false); } [OnDeserialized] internal void OnDeserializedMethod(StreamingContext context) { if (!_observing) SetObserving(true); } protected virtual void Init() { LoadedColumns = CreateLoadedColumns(); CheckSequence(); } private void CheckSequence() { if (this is ISequenceable seq && seq.Sequence <= 0) { seq.Sequence = CoreUtils.GenerateSequence(); } } #region Observing Flags public static bool GlobalObserving = true; private bool _observing = false; public bool IsObserving() { return GlobalObserving && _observing; } public void SetObserving(bool active) { bApplyingChanges = true; _observing = active; _disabledInterceptor = true; foreach (var oo in DatabaseSchema.GetSubObjects(this)) oo.SetObserving(active); _disabledInterceptor = false; bApplyingChanges = false; } protected virtual void DoPropertyChanged(string name, object? before, object? after) { } public event PropertyChangedEventHandler? PropertyChanged; private bool bApplyingChanges; private bool bChanged; private IOriginalValues? _originalValues; [DoNotPersist] [DoNotSerialize] public IOriginalValues OriginalValueList { get { _originalValues ??= CreateOriginalValues(); return _originalValues; } } [DoNotPersist] public ConcurrentDictionary? OriginalValues { get { if(OriginalValueList is OriginalValues v) { return v.Dictionary; } else { return null; } } set { if(value != null && OriginalValueList is OriginalValues v) { v.Dictionary = value; } } } [DoNotPersist] [DoNotSerialize] [JsonIgnore] public ILoadedColumns LoadedColumns { get; set; } protected virtual void SetChanged(string name, object? before, object? after) { bChanged = true; if (!bApplyingChanges) { try { bApplyingChanges = true; DoPropertyChanged(name, before, after); PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); } catch (Exception) { } bApplyingChanges = false; } } // This function is *only* meant to be called by EnclosedEntity and EntityLink internal void CascadePropertyChanged(string name, object? before, object? after) { SetChanged(name, before, after); } private bool QueryChanged() { if (OriginalValueList.Any()) return true; _disabledInterceptor = true; foreach (var oo in DatabaseSchema.GetSubObjects(this)) if (oo.IsChanged()) { _disabledInterceptor = false; return true; } _disabledInterceptor = false; return false; } public void OnPropertyChanged(string name, object? before, object? after) { if (!IsObserving()) return; if (name.Equals("IsChanged") || name.Equals("Observing") || name.Equals("OriginalValues")) return; LoadedColumns.Add(name); if (!BaseObjectExtensions.HasChanged(before, after)) return; if (!OriginalValueList.ContainsKey(name)) OriginalValueList[name] = before; SetChanged(name, before, after); } protected virtual IOriginalValues CreateOriginalValues() { return new OriginalValues(); } protected virtual ILoadedColumns CreateLoadedColumns() { return new LoadedColumns(); } public bool IsChanged() { return IsObserving() ? QueryChanged() : bChanged; } public void CancelChanges() { bApplyingChanges = true; var bObs = IsObserving(); SetObserving(false); foreach (var (key, value) in OriginalValueList) { try { var prop = DatabaseSchema.Property(GetType(), key); if(prop != null) { prop.Setter()(this, value); } else { Logger.Send(LogType.Error, "", $"'{key}' is not a property of {GetType().Name}"); } } catch (Exception e) { Logger.Send(LogType.Error, "", string.Format("*** Unknown Error: {0}\n{1}", e.Message, e.StackTrace)); } } OriginalValueList.Clear(); bChanged = false; _disabledInterceptor = true; foreach (var oo in DatabaseSchema.GetSubObjects(this)) { oo.CancelChanges(); } _disabledInterceptor = false; SetObserving(bObs); bApplyingChanges = false; } public void CommitChanges() { bApplyingChanges = true; OriginalValueList.Clear(); bChanged = false; _disabledInterceptor = true; foreach (var oo in DatabaseSchema.GetSubObjects(this)) oo.CommitChanges(); _disabledInterceptor = false; bApplyingChanges = false; } public string ChangedValues() { var result = new List(); var type = GetType(); try { foreach (var (key, _) in OriginalValueList) try { if (UserProperties.ContainsKey(key)) { var obj = UserProperties[key]; result.Add(string.Format("[{0} = {1}]", key, obj != null ? obj.ToString() : "null")); } else { var prop = DatabaseSchema.Property(type, key);// GetType().GetProperty(key); if (prop is StandardProperty standard && standard.Loggable != null) { /*var attribute = //prop.GetCustomAttributes(typeof(LoggablePropertyAttribute), true).FirstOrDefault(); if (attribute != null) {*/ //var lpa = (LoggablePropertyAttribute)attribute; var format = standard.Loggable.Format; var value = standard.Getter()(this); if (string.IsNullOrEmpty(format)) result.Add($"[{key} = {value}]"); else result.Add(string.Format("[{0} = {1:" + format + "}]", key, value)); //} } } } catch (Exception e) { Logger.Send(LogType.Error, "", string.Format("*** Unknown Error: {0}\n{1}", e.Message, e.StackTrace)); } } catch (Exception e) { Logger.Send(LogType.Error, "", string.Format("*** Unknown Error: {0}\n{1}", e.Message, e.StackTrace)); } return string.Join(" ", result); } #endregion #region UserProperties private UserProperties? _userproperties; private static readonly Dictionary> DefaultProperties = new Dictionary>(); [DoNotPersist] public UserProperties UserProperties { get { if (_userproperties == null) { _userproperties = new UserProperties(); var type = GetType(); if (!DefaultProperties.TryGetValue(type, out var defaultProps)) { defaultProps = new Dictionary(); var props = DatabaseSchema.Properties(type).Where(x => x is CustomProperty); foreach (var field in props) defaultProps[field.Name] = DatabaseSchema.DefaultValue(field.PropertyType); DefaultProperties[type] = defaultProps; } _userproperties.LoadFromDictionary(defaultProps); _userproperties.OnPropertyChanged += (o, n, b, a) => { if (IsObserving()) OnPropertyChanged(n, b, a); }; } return _userproperties; } } #endregion } public class BaseObjectSnapshot where T : BaseObject { private readonly List<(IProperty, object?)> Values; private readonly T Object; public BaseObjectSnapshot(T obj) { Values = new List<(IProperty, object?)>(); foreach(var property in DatabaseSchema.Properties(obj.GetType())) { Values.Add((property, property.Getter()(obj))); } Object = obj; } public void ResetObject() { Object.CancelChanges(); var bObs = Object.IsObserving(); Object.SetObserving(false); foreach(var (prop, value) in Values) { var oldValue = prop.Getter()(Object); prop.Setter()(Object, value); if(BaseObjectExtensions.HasChanged(oldValue, value)) { Object.OriginalValueList[prop.Name] = oldValue; } } Object.SetObserving(bObs); } } public static class BaseObjectExtensions { public static T Clone(this T obj) where T : BaseObject, new() { var newObj = new T(); obj._disabledInterceptor = true; foreach(var property in DatabaseSchema.Properties(obj.GetType())) { if (property.Parent != null && property.Parent.NullSafeGetter()(obj) is null) continue; property.Setter()(newObj, property.Getter()(obj)); } obj._disabledInterceptor = false; return newObj; } public static bool HasChanged(object? before, object? after) { if ((before == null || before.Equals("")) && (after == null || after.Equals(""))) return false; if (before == null != (after == null)) return true; if (!before!.GetType().Equals(after!.GetType())) return true; if (before is string[] && after is string[]) return !(before as string[]).SequenceEqual(after as string[]); return !before.Equals(after); } public static bool HasColumn(this T sender, string column) where T : BaseObject { return sender.LoadedColumns.Contains(column); } public static bool HasColumn(this T sender, Expression> column) where T : BaseObject { return sender.LoadedColumns.Contains(CoreUtils.GetFullPropertyName(column, ".")); } public static bool HasOriginalValue(this T sender, string propertyname) where T : BaseObject { return sender.OriginalValueList != null && sender.OriginalValueList.ContainsKey(propertyname); } public static TType GetOriginalValue(this T sender, string propertyname) where T : BaseObject { return sender.OriginalValueList != null && sender.OriginalValueList.ContainsKey(propertyname) ? (TType)CoreUtils.ChangeType(sender.OriginalValueList[propertyname], typeof(TType)) : default; } /// /// Get all database values (i.e., non-calculated, local properties) for a given object . /// If is , only retrieve values which have changed. /// public static Dictionary GetValues(this T sender, bool all) where T : BaseObject { var result = new Dictionary(); foreach(var property in DatabaseSchema.Properties(sender.GetType())) { if (property.IsDBColumn && (all || sender.HasOriginalValue(property.Name))) { result[property.Name] = property.Getter()(sender); } } return result; } public static Dictionary GetOriginaValues(this T sender) where T : BaseObject { var result = new Dictionary(); foreach(var property in DatabaseSchema.Properties(sender.GetType())) { if (property.IsDBColumn && sender.OriginalValueList.TryGetValue(property.Name, out var value)) { result[property.Name] = value; } } return result; } public static BaseObjectSnapshot TakeSnapshot(this T obj) where T : BaseObject { return new BaseObjectSnapshot(obj); } public static List Compare(this T sender, Dictionary original) where T : BaseObject { var result = new List(); var current = GetValues(sender, true); foreach (var key in current.Keys) if (original.ContainsKey(key)) { if (current[key] == null) { if (original[key] != null) result.Add(string.Format("[{0}] has changed from [{1}] to [{2}]", key, original[key], current[key])); } else { if (!current[key].Equals(original[key])) result.Add(string.Format("[{0}] has changed from [{1}] to [{2}]", key, original[key], current[key])); } } else { result.Add(string.Format("[{0}] not present in previous dictionary!", key)); } return result; } public static bool GetValue(this T sender, Expression> property, bool original, out TType result) where T : BaseObject { if (sender.HasOriginalValue(property)) { if (original) result = sender.GetOriginalValue(property); else { var expr = property.Compile(); result = expr(sender); } return true; } result = default(TType); return false; } public static void SetOriginalValue(this T sender, string propertyname, TType value) where T : BaseObject { sender.OriginalValueList[propertyname] = value; } public static bool HasOriginalValue(this T sender, Expression> property) where T : BaseObject { //var prop = ((MemberExpression)property.Body).Member as PropertyInfo; String propname = CoreUtils.GetFullPropertyName(property, "."); return !String.IsNullOrWhiteSpace(propname) && sender.OriginalValueList != null && sender.OriginalValueList.ContainsKey(propname); } public static TType GetOriginalValue(this T sender, Expression> property) where T : BaseObject { var prop = ((MemberExpression)property.Body).Member as PropertyInfo; return prop != null && sender.OriginalValueList != null && sender.OriginalValueList.ContainsKey(prop.Name) ? (TType)CoreUtils.ChangeType(sender.OriginalValueList[prop.Name], typeof(TType)) : default; } public static TType GetOriginalValue(this T sender, Expression> property, TType defaultValue) where T : BaseObject { var prop = ((MemberExpression)property.Body).Member as PropertyInfo; return prop != null && sender.OriginalValueList != null && sender.OriginalValueList.ContainsKey(prop.Name) ? (TType)CoreUtils.ChangeType(sender.OriginalValueList[prop.Name], typeof(TType)) : defaultValue; } public static void SetOriginalValue(this T sender, Expression> property, TType value) where T : BaseObject { var prop = ((MemberExpression)property.Body).Member as PropertyInfo; sender.OriginalValueList[prop.Name] = value; } } /// /// An is loggable if it has the defined on it.
/// If it is part of an , then it is only loggable if the property on the parent class /// also has . ///
public class LoggablePropertyAttribute : Attribute { public string Format { get; set; } } }