BaseObject.cs 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681
  1. using Newtonsoft.Json;
  2. using System;
  3. using System.Collections;
  4. using System.Collections.Concurrent;
  5. using System.Collections.Generic;
  6. using System.ComponentModel;
  7. using System.Data;
  8. using System.Linq;
  9. using System.Linq.Expressions;
  10. using System.Reflection;
  11. using System.Runtime.CompilerServices;
  12. using System.Runtime.Serialization;
  13. //using PropertyChanged;
  14. namespace InABox.Core
  15. {
  16. public class DoNotSerialize : Attribute
  17. {
  18. }
  19. public interface IBaseObject
  20. {
  21. public bool IsChanged();
  22. public void CancelChanges();
  23. public void CommitChanges();
  24. public bool IsObserving();
  25. public void SetObserving(bool active);
  26. }
  27. public interface IOriginalValues : IEnumerable<KeyValuePair<string, object?>>
  28. {
  29. public object? this[string key] { get; set; }
  30. public bool ContainsKey(string key);
  31. public void Clear();
  32. public bool TryGetValue(string key, out object? value);
  33. public bool TryAdd(string key, object? value);
  34. public void Remove(string key);
  35. public object? GetValueOrDefault(string key)
  36. {
  37. if(TryGetValue(key, out object? value)) return value;
  38. return null;
  39. }
  40. }
  41. public class OriginalValues : IOriginalValues
  42. {
  43. public OriginalValues()
  44. {
  45. }
  46. public ConcurrentDictionary<string, object?> Dictionary { get; set; } = new ConcurrentDictionary<string, object?>();
  47. public object? this[string key] { get => Dictionary[key]; set => Dictionary[key] = value; }
  48. public void Clear()
  49. {
  50. Dictionary.Clear();
  51. }
  52. public bool ContainsKey(string key) => Dictionary.ContainsKey(key);
  53. public IEnumerator<KeyValuePair<string, object?>> GetEnumerator()
  54. {
  55. return Dictionary.GetEnumerator();
  56. }
  57. public bool TryGetValue(string key, out object? value)
  58. {
  59. return Dictionary.TryGetValue(key, out value);
  60. }
  61. public bool TryAdd(string key, object? value)
  62. {
  63. return Dictionary.TryAdd(key, value);
  64. }
  65. public void Remove(string key)
  66. {
  67. (Dictionary as IDictionary<string, object?>).Remove(key);
  68. }
  69. IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
  70. }
  71. public interface ILoadedColumns : IEnumerable<string>
  72. {
  73. public bool Add(string key);
  74. public bool Contains(string key);
  75. }
  76. public class LoadedColumns : ILoadedColumns
  77. {
  78. private HashSet<string> Columns = new HashSet<string>();
  79. public bool Add(string key)
  80. {
  81. return Columns.Add(key);
  82. }
  83. public bool Contains(string key)
  84. {
  85. return Columns.Contains(key);
  86. }
  87. public IEnumerator<string> GetEnumerator()
  88. {
  89. return Columns.GetEnumerator();
  90. }
  91. IEnumerator IEnumerable.GetEnumerator()
  92. {
  93. return Columns.GetEnumerator();
  94. }
  95. }
  96. /// <summary>
  97. /// Observable object with INotifyPropertyChanged implemented
  98. /// </summary>
  99. public abstract class BaseObject : INotifyPropertyChanged, IBaseObject
  100. {
  101. public BaseObject()
  102. {
  103. SetObserving(false);
  104. DatabaseSchema.InitializeSubObjects(this);
  105. Init();
  106. SetObserving(true);
  107. }
  108. [OnDeserializing]
  109. internal void OnDeserializingMethod(StreamingContext context)
  110. {
  111. if (_observing)
  112. SetObserving(false);
  113. }
  114. [OnDeserialized]
  115. internal void OnDeserializedMethod(StreamingContext context)
  116. {
  117. if (!_observing)
  118. SetObserving(true);
  119. }
  120. protected virtual void Init()
  121. {
  122. LoadedColumns = CreateLoadedColumns();
  123. UserProperties = new UserProperties();
  124. //UserProperties.ParentType = this.GetType();
  125. DatabaseSchema.InitializeObject(this);
  126. UserProperties.OnPropertyChanged += (o, n, b, a) =>
  127. {
  128. if (IsObserving())
  129. OnPropertyChanged(n, b, a);
  130. };
  131. CheckSequence();
  132. }
  133. private void CheckSequence()
  134. {
  135. if (this is ISequenceable seq && seq.Sequence <= 0)
  136. {
  137. seq.Sequence = CoreUtils.GenerateSequence();
  138. }
  139. }
  140. #region Observing Flags
  141. public static bool GlobalObserving = true;
  142. private bool _observing = false;
  143. public bool IsObserving()
  144. {
  145. return GlobalObserving && _observing;
  146. }
  147. public void SetObserving(bool active)
  148. {
  149. bApplyingChanges = true;
  150. _observing = active;
  151. foreach (var oo in DatabaseSchema.GetSubObjects(this))
  152. oo.SetObserving(active);
  153. bApplyingChanges = false;
  154. }
  155. protected virtual void DoPropertyChanged(string name, object? before, object? after)
  156. {
  157. }
  158. public event PropertyChangedEventHandler? PropertyChanged;
  159. private bool bApplyingChanges;
  160. private bool bChanged;
  161. private IOriginalValues _originalValues;
  162. [DoNotPersist]
  163. [DoNotSerialize]
  164. public IOriginalValues OriginalValueList
  165. {
  166. get
  167. {
  168. _originalValues ??= CreateOriginalValues();
  169. return _originalValues;
  170. }
  171. }
  172. [DoNotPersist]
  173. public ConcurrentDictionary<string, object?>? OriginalValues
  174. {
  175. get
  176. {
  177. if(OriginalValueList is OriginalValues v)
  178. {
  179. return v.Dictionary;
  180. }
  181. else
  182. {
  183. return null;
  184. }
  185. }
  186. set
  187. {
  188. if(value != null && OriginalValueList is OriginalValues v)
  189. {
  190. v.Dictionary = value;
  191. }
  192. }
  193. }
  194. [DoNotPersist]
  195. [DoNotSerialize]
  196. [JsonIgnore]
  197. public ILoadedColumns LoadedColumns { get; set; }
  198. protected virtual void SetChanged(string name, object? before, object? after)
  199. {
  200. bChanged = true;
  201. if (!bApplyingChanges)
  202. {
  203. try
  204. {
  205. bApplyingChanges = true;
  206. DoPropertyChanged(name, before, after);
  207. PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
  208. }
  209. catch (Exception e)
  210. {
  211. }
  212. bApplyingChanges = false;
  213. }
  214. }
  215. // This function is *only* meant to be called by EnclosedEntity and EntityLink
  216. internal void CascadePropertyChanged(string name, object? before, object? after)
  217. {
  218. SetChanged(name, before, after);
  219. }
  220. private bool QueryChanged()
  221. {
  222. if (OriginalValueList.Any())
  223. return true;
  224. foreach (var oo in DatabaseSchema.GetSubObjects(this))
  225. if (oo.IsChanged())
  226. return true;
  227. return false;
  228. }
  229. public void OnPropertyChanged(string name, object? before, object? after)
  230. {
  231. if (!IsObserving())
  232. return;
  233. if (name.Equals("IsChanged"))
  234. return;
  235. if (name.Equals("Observing"))
  236. return;
  237. if (name.Equals("OriginalValues"))
  238. return;
  239. LoadedColumns.Add(name);
  240. if (!BaseObjectExtensions.HasChanged(before, after))
  241. return;
  242. if (!OriginalValueList.ContainsKey(name))
  243. OriginalValueList[name] = before;
  244. SetChanged(name, before, after);
  245. }
  246. protected virtual IOriginalValues CreateOriginalValues()
  247. {
  248. return new OriginalValues();
  249. }
  250. protected virtual ILoadedColumns CreateLoadedColumns()
  251. {
  252. return new LoadedColumns();
  253. }
  254. public bool IsChanged()
  255. {
  256. return IsObserving() ? QueryChanged() : bChanged;
  257. }
  258. public void CancelChanges()
  259. {
  260. bApplyingChanges = true;
  261. var bObs = IsObserving();
  262. SetObserving(false);
  263. foreach (var (key, value) in OriginalValueList)
  264. {
  265. try
  266. {
  267. var prop = DatabaseSchema.Property(GetType(), key);
  268. if(prop != null)
  269. {
  270. prop.Setter()(this, value);
  271. }
  272. else
  273. {
  274. Logger.Send(LogType.Error, "", $"'{key}' is not a property of {GetType().Name}");
  275. }
  276. }
  277. catch (Exception e)
  278. {
  279. Logger.Send(LogType.Error, "", string.Format("*** Unknown Error: {0}\n{1}", e.Message, e.StackTrace));
  280. }
  281. }
  282. OriginalValueList.Clear();
  283. bChanged = false;
  284. foreach (var oo in DatabaseSchema.GetSubObjects(this)) oo.CancelChanges();
  285. SetObserving(bObs);
  286. bApplyingChanges = false;
  287. }
  288. public void CommitChanges()
  289. {
  290. bApplyingChanges = true;
  291. OriginalValueList.Clear();
  292. bChanged = false;
  293. foreach (var oo in DatabaseSchema.GetSubObjects(this))
  294. oo.CommitChanges();
  295. bApplyingChanges = false;
  296. }
  297. public string ChangedValues()
  298. {
  299. var result = new List<string>();
  300. var type = GetType();
  301. try
  302. {
  303. foreach (var (key, _) in OriginalValueList)
  304. try
  305. {
  306. if (UserProperties.ContainsKey(key))
  307. {
  308. var obj = UserProperties[key];
  309. result.Add(string.Format("[{0} = {1}]", key, obj != null ? obj.ToString() : "null"));
  310. }
  311. else
  312. {
  313. var prop = DatabaseSchema.Property(type, key);// GetType().GetProperty(key);
  314. if (prop is StandardProperty standard && standard.Loggable != null)
  315. {
  316. /*var attribute = //prop.GetCustomAttributes(typeof(LoggablePropertyAttribute), true).FirstOrDefault();
  317. if (attribute != null)
  318. {*/
  319. //var lpa = (LoggablePropertyAttribute)attribute;
  320. var format = standard.Loggable.Format;
  321. var value = standard.Getter()(this);
  322. if (string.IsNullOrEmpty(format))
  323. result.Add($"[{key} = {value}]");
  324. else
  325. result.Add(string.Format("[{0} = {1:" + format + "}]", key, value));
  326. //}
  327. }
  328. }
  329. }
  330. catch (Exception e)
  331. {
  332. Logger.Send(LogType.Error, "", string.Format("*** Unknown Error: {0}\n{1}", e.Message, e.StackTrace));
  333. }
  334. }
  335. catch (Exception e)
  336. {
  337. Logger.Send(LogType.Error, "", string.Format("*** Unknown Error: {0}\n{1}", e.Message, e.StackTrace));
  338. }
  339. return string.Join(" ", result);
  340. }
  341. #endregion
  342. #region UserProperties
  343. private UserProperties _userproperties;
  344. [DoNotPersist]
  345. public UserProperties UserProperties
  346. {
  347. get
  348. {
  349. CheckUserProperties();
  350. return _userproperties;
  351. }
  352. set
  353. {
  354. _userproperties = value;
  355. CheckUserProperties();
  356. }
  357. }
  358. private static Dictionary<string, object?>? DefaultProperties;
  359. private void CheckUserProperties()
  360. {
  361. if (_userproperties == null)
  362. {
  363. _userproperties = new UserProperties();
  364. if (DefaultProperties == null)
  365. {
  366. DefaultProperties = new Dictionary<string, object?>();
  367. var props = DatabaseSchema.Properties(GetType()).Where(x => x is CustomProperty).ToArray();
  368. foreach (var field in props)
  369. DefaultProperties[field.Name] = DatabaseSchema.DefaultValue(field.PropertyType);
  370. }
  371. _userproperties.LoadFromDictionary(DefaultProperties);
  372. }
  373. }
  374. #endregion
  375. }
  376. public class BaseObjectSnapshot<T>
  377. where T : BaseObject
  378. {
  379. private readonly List<(IProperty, object?)> Values;
  380. private readonly T Object;
  381. public BaseObjectSnapshot(T obj)
  382. {
  383. Values = new List<(IProperty, object?)>();
  384. foreach(var property in DatabaseSchema.Properties(obj.GetType()))
  385. {
  386. Values.Add((property, property.Getter()(obj)));
  387. }
  388. Object = obj;
  389. }
  390. public void ResetObject()
  391. {
  392. Object.CancelChanges();
  393. var bObs = Object.IsObserving();
  394. Object.SetObserving(false);
  395. foreach(var (prop, value) in Values)
  396. {
  397. var oldValue = prop.Getter()(Object);
  398. prop.Setter()(Object, value);
  399. if(BaseObjectExtensions.HasChanged(oldValue, value))
  400. {
  401. Object.OriginalValueList[prop.Name] = oldValue;
  402. }
  403. }
  404. Object.SetObserving(bObs);
  405. }
  406. }
  407. public static class BaseObjectExtensions
  408. {
  409. public static bool HasChanged(object? before, object? after)
  410. {
  411. if ((before == null || before.Equals("")) && (after == null || after.Equals("")))
  412. return false;
  413. if (before == null != (after == null))
  414. return true;
  415. if (!before!.GetType().Equals(after!.GetType()))
  416. return true;
  417. if (before is string[] && after is string[])
  418. return !(before as string[]).SequenceEqual(after as string[]);
  419. return !before.Equals(after);
  420. }
  421. public static bool HasColumn<T>(this T sender, string column)
  422. where T : BaseObject
  423. {
  424. return sender.LoadedColumns.Contains(column);
  425. }
  426. public static bool HasColumn<T, TType>(this T sender, Expression<Func<T, TType>> column)
  427. where T : BaseObject
  428. {
  429. return sender.LoadedColumns.Contains(CoreUtils.GetFullPropertyName(column, "."));
  430. }
  431. public static bool HasOriginalValue<T>(this T sender, string propertyname) where T : BaseObject
  432. {
  433. return sender.OriginalValueList != null && sender.OriginalValueList.ContainsKey(propertyname);
  434. }
  435. public static TType GetOriginalValue<T, TType>(this T sender, string propertyname) where T : BaseObject
  436. {
  437. return sender.OriginalValueList != null && sender.OriginalValueList.ContainsKey(propertyname)
  438. ? (TType)CoreUtils.ChangeType(sender.OriginalValueList[propertyname], typeof(TType))
  439. : default;
  440. }
  441. /// <summary>
  442. /// Get all database values (i.e., non-calculated, local properties) for a given object <paramref name="sender"/>.
  443. /// If <paramref name="all"/> is <see langword="false"/>, only retrieve values which have changed.
  444. /// </summary>
  445. public static Dictionary<string, object?> GetValues<T>(this T sender, bool all) where T : BaseObject
  446. {
  447. var result = new Dictionary<string, object?>();
  448. foreach(var property in DatabaseSchema.Properties(sender.GetType()))
  449. {
  450. if (property.IsDBColumn)
  451. {
  452. var isLocal = !property.HasParentEntityLink()
  453. || (property.Parent?.HasParentEntityLink() != true && property.Name.EndsWith(".ID"));
  454. if (isLocal && (all || sender.HasOriginalValue(property.Name)))
  455. {
  456. result[property.Name] = property.Getter()(sender);
  457. }
  458. }
  459. }
  460. return result;
  461. }
  462. public static Dictionary<string, object?> GetOriginaValues<T>(this T sender) where T : BaseObject
  463. {
  464. var result = new Dictionary<string, object?>();
  465. foreach(var property in DatabaseSchema.Properties(sender.GetType()))
  466. {
  467. if (property.IsDBColumn)
  468. {
  469. var isLocal = !property.HasParentEntityLink()
  470. || (property.Parent?.HasParentEntityLink() != true && property.Name.EndsWith(".ID"));
  471. if (isLocal && sender.OriginalValueList.TryGetValue(property.Name, out var value))
  472. {
  473. result[property.Name] = value;
  474. }
  475. }
  476. }
  477. return result;
  478. }
  479. public static BaseObjectSnapshot<T> TakeSnapshot<T>(this T obj)
  480. where T : BaseObject
  481. {
  482. return new BaseObjectSnapshot<T>(obj);
  483. }
  484. public static List<string> Compare<T>(this T sender, Dictionary<string, object> original) where T : BaseObject
  485. {
  486. var result = new List<string>();
  487. var current = GetValues(sender, true);
  488. foreach (var key in current.Keys)
  489. if (original.ContainsKey(key))
  490. {
  491. if (current[key] == null)
  492. {
  493. if (original[key] != null)
  494. result.Add(string.Format("[{0}] has changed from [{1}] to [{2}]", key, original[key], current[key]));
  495. }
  496. else
  497. {
  498. if (!current[key].Equals(original[key]))
  499. result.Add(string.Format("[{0}] has changed from [{1}] to [{2}]", key, original[key], current[key]));
  500. }
  501. }
  502. else
  503. {
  504. result.Add(string.Format("[{0}] not present in previous dictionary!", key));
  505. }
  506. return result;
  507. }
  508. public static bool GetValue<T,TType>(this T sender, Expression<Func<T,TType>> property, bool original, out TType result) where T : BaseObject
  509. {
  510. if (sender.HasOriginalValue<T, TType>(property))
  511. {
  512. if (original)
  513. result = sender.GetOriginalValue<T, TType>(property);
  514. else
  515. {
  516. var expr = property.Compile();
  517. result = expr(sender);
  518. }
  519. return true;
  520. }
  521. result = default(TType);
  522. return false;
  523. }
  524. public static void SetOriginalValue<T, TType>(this T sender, string propertyname, TType value) where T : BaseObject
  525. {
  526. sender.OriginalValueList[propertyname] = value;
  527. }
  528. public static bool HasOriginalValue<T, TType>(this T sender, Expression<Func<T, TType>> property) where T : BaseObject
  529. {
  530. //var prop = ((MemberExpression)property.Body).Member as PropertyInfo;
  531. String propname = CoreUtils.GetFullPropertyName(property, ".");
  532. return !String.IsNullOrWhiteSpace(propname) && sender.OriginalValueList != null && sender.OriginalValueList.ContainsKey(propname);
  533. }
  534. public static TType GetOriginalValue<T, TType>(this T sender, Expression<Func<T, TType>> property) where T : BaseObject
  535. {
  536. var prop = ((MemberExpression)property.Body).Member as PropertyInfo;
  537. return prop != null && sender.OriginalValueList != null && sender.OriginalValueList.ContainsKey(prop.Name)
  538. ? (TType)CoreUtils.ChangeType(sender.OriginalValueList[prop.Name], typeof(TType))
  539. : default;
  540. }
  541. public static TType GetOriginalValue<T, TType>(this T sender, Expression<Func<T, TType>> property, TType defaultValue) where T : BaseObject
  542. {
  543. var prop = ((MemberExpression)property.Body).Member as PropertyInfo;
  544. return prop != null && sender.OriginalValueList != null && sender.OriginalValueList.ContainsKey(prop.Name)
  545. ? (TType)CoreUtils.ChangeType(sender.OriginalValueList[prop.Name], typeof(TType))
  546. : defaultValue;
  547. }
  548. public static void SetOriginalValue<T, TType>(this T sender, Expression<Func<T, TType>> property, TType value) where T : BaseObject
  549. {
  550. var prop = ((MemberExpression)property.Body).Member as PropertyInfo;
  551. sender.OriginalValueList[prop.Name] = value;
  552. }
  553. }
  554. /// <summary>
  555. /// An <see cref="IProperty"/> is loggable if it has the <see cref="LoggablePropertyAttribute"/> defined on it.<br/>
  556. /// If it is part of an <see cref="IEntityLink"/>, then it is only loggable if the <see cref="IEntityLink"/> property on the parent class
  557. /// also has <see cref="LoggablePropertyAttribute"/>.
  558. /// </summary>
  559. public class LoggablePropertyAttribute : Attribute
  560. {
  561. public string Format { get; set; }
  562. }
  563. }