BaseObject.cs 25 KB

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