DataType refactoring preparation - Entity refactoring

This commit is contained in:
Stephan
2018-01-15 11:32:30 +01:00
parent 988aa661ea
commit d23933a5b1
213 changed files with 2149 additions and 2478 deletions

View File

@@ -0,0 +1,37 @@
using System.Collections.Generic;
using System.Reflection;
namespace Umbraco.Core.Models.Entities
{
/// <summary>
/// Provides a concrete implementation of <see cref="BeingDirtyBase"/>.
/// </summary>
/// <remarks>
/// <para>This class is provided for classes that cannot inherit from <see cref="BeingDirtyBase"/>
/// and therefore need to implement <see cref="IRememberBeingDirty"/>, by re-using some of
/// <see cref="BeingDirtyBase"/> logic.</para>
/// </remarks>
public sealed class BeingDirty : BeingDirtyBase
{
/// <summary>
/// Sets a property value, detects changes and manages the dirty flag.
/// </summary>
/// <typeparam name="T">The type of the value.</typeparam>
/// <param name="value">The new value.</param>
/// <param name="valueRef">A reference to the value to set.</param>
/// <param name="propertySelector">The property selector.</param>
/// <param name="comparer">A comparer to compare property values.</param>
public new void SetPropertyValueAndDetectChanges<T>(T value, ref T valueRef, PropertyInfo propertySelector, IEqualityComparer<T> comparer = null)
{
base.SetPropertyValueAndDetectChanges(value, ref valueRef, propertySelector, comparer);
}
/// <summary>
/// Registers that a property has changed.
/// </summary>
public new void OnPropertyChanged(PropertyInfo propertySelector)
{
base.OnPropertyChanged(propertySelector);
}
}
}

View File

@@ -0,0 +1,183 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Reflection;
using System.Runtime.Serialization;
namespace Umbraco.Core.Models.Entities
{
/// <summary>
/// Provides a base implementation of <see cref="ICanBeDirty"/> and <see cref="IRememberBeingDirty"/>.
/// </summary>
[Serializable]
[DataContract(IsReference = true)]
public abstract class BeingDirtyBase : IRememberBeingDirty
{
private bool _withChanges = true; // should we track changes?
private Dictionary<string, bool> _currentChanges; // which properties have changed?
private Dictionary<string, bool> _savedChanges; // which properties had changed at last commit?
#region ICanBeDirty
/// <inheritdoc />
public virtual bool IsDirty()
{
return _currentChanges != null && _currentChanges.Any();
}
/// <inheritdoc />
public virtual bool IsPropertyDirty(string propertyName)
{
return _currentChanges != null && _currentChanges.Any(x => x.Key == propertyName);
}
/// <inheritdoc />
public virtual IEnumerable<string> GetDirtyProperties()
{
// ReSharper disable once MergeConditionalExpression
return _currentChanges == null
? Enumerable.Empty<string>()
: _currentChanges.Where(x => x.Value).Select(x => x.Key);
}
/// <inheritdoc />
/// <remarks>Saves dirty properties so they can be checked with WasDirty.</remarks>
public virtual void ResetDirtyProperties()
{
ResetDirtyProperties(true);
}
#endregion
#region IRememberBeingDirty
/// <inheritdoc />
public virtual bool WasDirty()
{
return _savedChanges != null && _savedChanges.Any();
}
/// <inheritdoc />
public virtual bool WasPropertyDirty(string propertyName)
{
return _savedChanges != null && _savedChanges.Any(x => x.Key == propertyName);
}
/// <inheritdoc />
public void ResetWereDirtyProperties()
{
// note: cannot .Clear() because when memberwise-cloning this will be the SAME
// instance as the one on the clone, so we need to create a new instance.
_savedChanges = null;
}
/// <inheritdoc />
public virtual void ResetDirtyProperties(bool rememberDirty)
{
// capture changes if remembering
// clone the dictionary in case it's shared by an entity clone
_savedChanges = rememberDirty && _currentChanges != null
? _currentChanges.ToDictionary(v => v.Key, v => v.Value)
: null;
// note: cannot .Clear() because when memberwise-clone this will be the SAME
// instance as the one on the clone, so we need to create a new instance.
_currentChanges = null;
}
#endregion
#region Change Tracking
/// <summary>
/// Occurs when a property changes.
/// </summary>
public event PropertyChangedEventHandler PropertyChanged;
/// <summary>
/// Registers that a property has changed.
/// </summary>
protected virtual void OnPropertyChanged(PropertyInfo propertyInfo)
{
if (_withChanges == false)
return;
if (_currentChanges == null)
_currentChanges = new Dictionary<string, bool>();
_currentChanges[propertyInfo.Name] = true;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyInfo.Name));
}
/// <summary>
/// Disables change tracking.
/// </summary>
public void DisableChangeTracking()
{
_withChanges = false;
}
/// <summary>
/// Enables change tracking.
/// </summary>
public void EnableChangeTracking()
{
_withChanges = true;
}
/// <summary>
/// Sets a property value, detects changes and manages the dirty flag.
/// </summary>
/// <typeparam name="T">The type of the value.</typeparam>
/// <param name="value">The new value.</param>
/// <param name="valueRef">A reference to the value to set.</param>
/// <param name="propertySelector">The property selector.</param>
/// <param name="comparer">A comparer to compare property values.</param>
protected void SetPropertyValueAndDetectChanges<T>(T value, ref T valueRef, PropertyInfo propertySelector, IEqualityComparer<T> comparer = null)
{
if (comparer == null)
{
// if no comparer is provided, use the default provider, as long as the value is not
// an IEnumerable - exclude strings, which are IEnumerable but have a default comparer
var typeofT = typeof(T);
if (!(typeofT == typeof(string)) && typeof(IEnumerable).IsAssignableFrom(typeofT))
throw new ArgumentNullException(nameof(comparer), "A custom comparer must be supplied for IEnumerable values.");
comparer = EqualityComparer<T>.Default;
}
// compare values
var changed = _withChanges && comparer.Equals(valueRef, value) == false;
// assign the new value
valueRef = value;
// handle change
if (changed)
OnPropertyChanged(propertySelector);
}
/// <summary>
/// Detects changes and manages the dirty flag.
/// </summary>
/// <typeparam name="T">The type of the value.</typeparam>
/// <param name="value">The new value.</param>
/// <param name="orig">The original value.</param>
/// <param name="propertySelector">The property selector.</param>
/// <param name="comparer">A comparer to compare property values.</param>
/// <param name="changed">A value indicating whether we know values have changed and no comparison is required.</param>
protected void DetectChanges<T>(T value, T orig, PropertyInfo propertySelector, IEqualityComparer<T> comparer, bool changed)
{
// compare values
changed = _withChanges && (changed || !comparer.Equals(orig, value));
// handle change
if (changed)
OnPropertyChanged(propertySelector);
}
#endregion
}
}

View File

@@ -0,0 +1,17 @@
namespace Umbraco.Core.Models.Entities
{
/// <summary>
/// Implements <see cref="IContentEntitySlim"/>.
/// </summary>
public class ContentEntitySlim : EntitySlim, IContentEntitySlim
{
/// <inheritdoc />
public string ContentTypeAlias { get; set; }
/// <inheritdoc />
public string ContentTypeIcon { get; set; }
/// <inheritdoc />
public string ContentTypeThumbnail { get; set; }
}
}

View File

@@ -0,0 +1,14 @@
namespace Umbraco.Core.Models.Entities
{
/// <summary>
/// Implements <see cref="IDocumentEntitySlim"/>.
/// </summary>
public class DocumentEntitySlim : ContentEntitySlim, IDocumentEntitySlim
{
/// <inheritdoc />
public bool Published { get; set; }
/// <inheritdoc />
public bool Edited { get; set; }
}
}

View File

@@ -0,0 +1,180 @@
using System;
using System.Diagnostics;
using System.Reflection;
using System.Runtime.Serialization;
namespace Umbraco.Core.Models.Entities
{
/// <summary>
/// Provides a base class for entities.
/// </summary>
[Serializable]
[DataContract(IsReference = true)]
[DebuggerDisplay("Id: {" + nameof(Id) + "}")]
public abstract class EntityBase : BeingDirtyBase, IEntity
{
private static readonly Lazy<PropertySelectors> Ps = new Lazy<PropertySelectors>();
private bool _hasIdentity;
private int _id;
private Guid _key;
private DateTime _createDate;
private DateTime _updateDate;
// ReSharper disable once ClassNeverInstantiated.Local
private class PropertySelectors
{
public readonly PropertyInfo IdSelector = ExpressionHelper.GetPropertyInfo<EntityBase, int>(x => x.Id);
public readonly PropertyInfo KeySelector = ExpressionHelper.GetPropertyInfo<EntityBase, Guid>(x => x.Key);
public readonly PropertyInfo CreateDateSelector = ExpressionHelper.GetPropertyInfo<EntityBase, DateTime>(x => x.CreateDate);
public readonly PropertyInfo UpdateDateSelector = ExpressionHelper.GetPropertyInfo<EntityBase, DateTime>(x => x.UpdateDate);
}
/// <inheritdoc />
[DataMember]
public int Id
{
get => _id;
set
{
SetPropertyValueAndDetectChanges(value, ref _id, Ps.Value.IdSelector);
_hasIdentity = value != 0;
}
}
/// <inheritdoc />
[DataMember]
public Guid Key
{
get
{
// if an entity does NOT have a key yet, assign one now
if (_key == Guid.Empty)
_key = Guid.NewGuid();
return _key;
}
set => SetPropertyValueAndDetectChanges(value, ref _key, Ps.Value.KeySelector);
}
/// <inheritdoc />
[DataMember]
public DateTime CreateDate
{
get => _createDate;
set => SetPropertyValueAndDetectChanges(value, ref _createDate, Ps.Value.CreateDateSelector);
}
/// <inheritdoc />
[DataMember]
public DateTime UpdateDate
{
get => _updateDate;
set => SetPropertyValueAndDetectChanges(value, ref _updateDate, Ps.Value.UpdateDateSelector);
}
/// <inheritdoc />
[DataMember]
public DateTime? DeleteDate { get; set; } // no change tracking - not persisted
/// <inheritdoc />
[DataMember]
public virtual bool HasIdentity => _hasIdentity;
/// <summary>
/// Resets the entity identity.
/// </summary>
internal virtual void ResetIdentity()
{
_id = default;
_key = Guid.Empty;
_hasIdentity = false;
}
/// <summary>
/// Updates the entity when it is being saved for the first time.
/// </summary>
internal virtual void AddingEntity()
{
var now = DateTime.Now;
// set the create and update dates, if not already set
if (IsPropertyDirty("CreateDate") == false || _createDate == default)
CreateDate = now;
if (IsPropertyDirty("UpdateDate") == false || _updateDate == default)
UpdateDate = now;
}
/// <summary>
/// Updates the entity when it is being saved.
/// </summary>
internal virtual void UpdatingEntity()
{
var now = DateTime.Now;
// just in case
if (_createDate == default)
CreateDate = now;
// set the update date if not already set
if (IsPropertyDirty("UpdateDate") == false || _updateDate == default)
UpdateDate = now;
}
public virtual bool Equals(EntityBase other)
{
return other != null && (ReferenceEquals(this, other) || SameIdentityAs(other));
}
public override bool Equals(object obj)
{
return obj != null && (ReferenceEquals(this, obj) || SameIdentityAs(obj as EntityBase));
}
private bool SameIdentityAs(EntityBase other)
{
if (other == null) return false;
// same identity if
// - same object (reference equals)
// - or same Clr type, both have identities, and they are identical
if (ReferenceEquals(this, other))
return true;
return GetType() == other.GetType() && HasIdentity && other.HasIdentity && Id == other.Id;
}
public override int GetHashCode()
{
unchecked
{
var hashCode = HasIdentity.GetHashCode();
hashCode = (hashCode * 397) ^ Id;
hashCode = (hashCode * 397) ^ GetType().GetHashCode();
return hashCode;
}
}
public virtual object DeepClone()
{
// memberwise-clone (ie shallow clone) the entity
var unused = Key; // ensure that 'this' has a key, before cloning
var clone = (EntityBase) MemberwiseClone();
// clear changes (ensures the clone has its own dictionaries)
// then disable change tracking
clone.ResetDirtyProperties(false);
clone.DisableChangeTracking();
// deep clone ref properties that are IDeepCloneable
DeepCloneHelper.DeepCloneRefProperties(this, clone);
// clear changes again (just to be sure, because we were not tracking)
// then enable change tracking
clone.ResetDirtyProperties(false);
clone.EnableChangeTracking();
return clone;
}
}
}

View File

@@ -0,0 +1,220 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using Umbraco.Core.Exceptions;
namespace Umbraco.Core.Models.Entities
{
// fixme - changing the name of some properties that were in additionalData => must update corresponding javascript?
/// <summary>
/// Implementation of <see cref="IEntitySlim"/> for internal use.
/// </summary>
/// <remarks>
/// <para>Although it implements <see cref="IEntitySlim"/>, this class does not
/// implement <see cref="IRememberBeingDirty"/> and everything this interface defines, throws.</para>
/// <para>Although it implements <see cref="IEntitySlim"/>, this class does not
/// implement <see cref="IDeepCloneable"/> and deep-cloning throws.</para>
/// </remarks>
public class EntitySlim : IEntitySlim
{
private IDictionary<string, object> _additionalData;
/// <summary>
/// Gets an entity representing "root".
/// </summary>
public static readonly IEntitySlim Root = new EntitySlim { Path = "-1", Name = "root", HasChildren = true };
// implement IEntity
/// <inheritdoc />
[DataMember]
public int Id { get; set; }
/// <inheritdoc />
[DataMember]
public Guid Key { get; set; }
/// <inheritdoc />
[DataMember]
public DateTime CreateDate { get; set; }
/// <inheritdoc />
[DataMember]
public DateTime UpdateDate { get; set; }
/// <inheritdoc />
[DataMember]
public DateTime? DeleteDate { get; set; }
/// <inheritdoc />
[DataMember]
public bool HasIdentity => Id != 0;
// implement ITreeEntity
/// <inheritdoc />
[DataMember]
public string Name { get; set; }
/// <inheritdoc />
[DataMember]
public int CreatorId { get; set; }
/// <inheritdoc />
[DataMember]
public int ParentId { get; set; }
/// <inheritdoc />
public void SetParent(ITreeEntity parent) => throw new WontImplementException();
/// <inheritdoc />
[DataMember]
public int Level { get; set; }
/// <inheritdoc />
[DataMember]
public string Path { get; set; }
/// <inheritdoc />
[DataMember]
public int SortOrder { get; set; }
/// <inheritdoc />
[DataMember]
public bool Trashed { get; set; }
// implement IUmbracoEntity
/// <inheritdoc />
[DataMember]
public IDictionary<string, object> AdditionalData => _additionalData ?? (_additionalData = new Dictionary<string, object>());
/// <inheritdoc />
[IgnoreDataMember]
public bool HasAdditionalData => _additionalData != null;
// implement IEntitySlim
/// <inheritdoc />
[DataMember]
public Guid NodeObjectType { get; set; }
/// <inheritdoc />
[DataMember]
public bool HasChildren { get; set; }
/// <inheritdoc />
[DataMember]
public virtual bool IsContainer { get; set; }
/// <summary>
/// Represents a lightweight property.
/// </summary>
public class PropertySlim
{
/// <summary>
/// Initializes a new instance of the <see cref="PropertySlim"/> class.
/// </summary>
public PropertySlim(string editorAlias, object value)
{
PropertyEditorAlias = editorAlias;
Value = value;
}
/// <summary>
/// Gets the property editor alias.
/// </summary>
public string PropertyEditorAlias { get; }
/// <summary>
/// Gets the property value.
/// </summary>
public object Value { get; }
protected bool Equals(PropertySlim other)
{
return PropertyEditorAlias.Equals(other.PropertyEditorAlias) && Equals(Value, other.Value);
}
public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
if (obj.GetType() != GetType()) return false;
return Equals((PropertySlim) obj);
}
public override int GetHashCode()
{
unchecked
{
return (PropertyEditorAlias.GetHashCode() * 397) ^ (Value != null ? Value.GetHashCode() : 0);
}
}
}
#region IDeepCloneable
/// <inheritdoc />
public object DeepClone()
{
throw new WontImplementException();
}
#endregion
#region IRememberBeingDirty
// IEntitySlim does *not* track changes, but since it indirectly implements IUmbracoEntity,
// and therefore IRememberBeingDirty, we have to have those methods - which all throw.
public bool IsDirty()
{
throw new WontImplementException();
}
public bool IsPropertyDirty(string propName)
{
throw new WontImplementException();
}
public IEnumerable<string> GetDirtyProperties()
{
throw new WontImplementException();
}
public void ResetDirtyProperties()
{
throw new WontImplementException();
}
public bool WasDirty()
{
throw new WontImplementException();
}
public bool WasPropertyDirty(string propertyName)
{
throw new WontImplementException();
}
public void ResetWereDirtyProperties()
{
throw new WontImplementException();
}
public void ResetDirtyProperties(bool rememberDirty)
{
throw new WontImplementException();
}
#endregion
}
}

View File

@@ -0,0 +1,30 @@
using System.Collections.Generic;
namespace Umbraco.Core.Models.Entities
{
/// <summary>
/// Defines an entity that tracks property changes and can be dirty.
/// </summary>
public interface ICanBeDirty
{
/// <summary>
/// Determines whether the current entity is dirty.
/// </summary>
bool IsDirty();
/// <summary>
/// Determines whether a specific property is dirty.
/// </summary>
bool IsPropertyDirty(string propName);
/// <summary>
/// Gets properties that are dirty.
/// </summary>
IEnumerable<string> GetDirtyProperties();
/// <summary>
/// Resets dirty properties.
/// </summary>
void ResetDirtyProperties();
}
}

View File

@@ -0,0 +1,23 @@
namespace Umbraco.Core.Models.Entities
{
/// <summary>
/// Represents a lightweight content entity, managed by the entity service.
/// </summary>
public interface IContentEntitySlim : IEntitySlim
{
/// <summary>
/// Gets the content type alias.
/// </summary>
string ContentTypeAlias { get; }
/// <summary>
/// Gets the content type icon.
/// </summary>
string ContentTypeIcon { get; }
/// <summary>
/// Gets the content type thumbnail.
/// </summary>
string ContentTypeThumbnail { get; }
}
}

View File

@@ -0,0 +1,18 @@
namespace Umbraco.Core.Models.Entities
{
/// <summary>
/// Represents a lightweight document entity, managed by the entity service.
/// </summary>
public interface IDocumentEntitySlim : IContentEntitySlim
{
/// <summary>
/// Gets a value indicating whether the document is published.
/// </summary>
bool Published { get; }
/// <summary>
/// Gets a value indicating whether the document has edited properties.
/// </summary>
bool Edited { get; }
}
}

View File

@@ -0,0 +1,45 @@
using System;
namespace Umbraco.Core.Models.Entities
{
/// <summary>
/// Defines an entity.
/// </summary>
public interface IEntity : IDeepCloneable
{
/// <summary>
/// Gets or sets the integer identifier of the entity.
/// </summary>
int Id { get; set; }
/// <summary>
/// Gets or sets the Guid unique identifier of the entity.
/// </summary>
Guid Key { get; set; }
/// <summary>
/// Gets or sets the creation date.
/// </summary>
DateTime CreateDate { get; set; }
/// <summary>
/// Gets or sets the last update date.
/// </summary>
DateTime UpdateDate { get; set; }
/// <summary>
/// Gets or sets the delete date.
/// </summary>
/// <remarks>
/// <para>The delete date is null when the entity has not been deleted.</para>
/// <para>The delete date has a value when the entity instance has been deleted, but this value
/// is transient and not persisted in database (since the entity does not exist anymore).</para>
/// </remarks>
DateTime? DeleteDate { get; set; }
/// <summary>
/// Gets a value indicating whether the entity has an identity.
/// </summary>
bool HasIdentity { get; }
}
}

View File

@@ -0,0 +1,25 @@
using System;
namespace Umbraco.Core.Models.Entities
{
/// <summary>
/// Represents a lightweight entity, managed by the entity service.
/// </summary>
public interface IEntitySlim : IUmbracoEntity
{
/// <summary>
/// Gets or sets the entity object type.
/// </summary>
Guid NodeObjectType { get; }
/// <summary>
/// Gets or sets a value indicating whether the entity has children.
/// </summary>
bool HasChildren { get; }
/// <summary>
/// Gets a value indicating whether the entity is a container.
/// </summary>
bool IsContainer { get; }
}
}

View File

@@ -0,0 +1,33 @@
namespace Umbraco.Core.Models.Entities
{
/// <summary>
/// Defines an entity that tracks property changes and can be dirty, and remembers
/// which properties were dirty when the changes were committed.
/// </summary>
public interface IRememberBeingDirty : ICanBeDirty
{
/// <summary>
/// Determines whether the current entity is dirty.
/// </summary>
/// <remarks>A property was dirty if it had been changed and the changes were committed.</remarks>
bool WasDirty();
/// <summary>
/// Determines whether a specific property was dirty.
/// </summary>
/// <remarks>A property was dirty if it had been changed and the changes were committed.</remarks>
bool WasPropertyDirty(string propertyName);
/// <summary>
/// Resets properties that were dirty.
/// </summary>
void ResetWereDirtyProperties();
/// <summary>
/// Resets dirty properties.
/// </summary>
/// <param name="rememberDirty">A value indicating whether to remember dirty properties.</param>
/// <remarks>When <paramref name="rememberDirty"/> is true, dirty properties are saved so they can be checked with WasDirty.</remarks>
void ResetDirtyProperties(bool rememberDirty);
}
}

View File

@@ -0,0 +1,56 @@
namespace Umbraco.Core.Models.Entities
{
/// <summary>
/// Defines an entity that belongs to a tree.
/// </summary>
public interface ITreeEntity : IEntity
{
/// <summary>
/// Gets or sets the name of the entity.
/// </summary>
string Name { get; set; }
/// <summary>
/// Gets or sets the identifier of the user who created this entity.
/// </summary>
int CreatorId { get; set; }
/// <summary>
/// Gets or sets the identifier of the parent entity.
/// </summary>
int ParentId { get; set; }
/// <summary>
/// Sets the parent entity.
/// </summary>
/// <remarks>Use this method to set the parent entity when the parent entity is known, but has not
/// been persistent and does not yet have an identity. The parent identifier will we retrieved
/// from the parent entity when needed. If the parent entity still does not have an entity by that
/// time, an exception will be thrown by <see cref="ParentId"/> getter.</remarks>
void SetParent(ITreeEntity parent);
/// <summary>
/// Gets or sets the level of the entity.
/// </summary>
int Level { get; set; }
/// <summary>
/// Gets or sets the path to the entity.
/// </summary>
string Path { get; set; }
/// <summary>
/// Gets or sets the sort order of the entity.
/// </summary>
int SortOrder { get; set; }
/// <summary>
/// Gets a value indicating whether this entity is trashed.
/// </summary>
/// <remarks>
/// <para>Trashed entities are located in the recycle bin.</para>
/// <para>Always false for entities that do not support being trashed.</para>
/// </remarks>
bool Trashed { get; }
}
}

View File

@@ -0,0 +1,29 @@
using System.Collections.Generic;
namespace Umbraco.Core.Models.Entities
{
/// <summary>
/// Represents an entity that can be managed by the entity service.
/// </summary>
/// <remarks>
/// <para>An IUmbracoEntity can be related to another via the IRelationService.</para>
/// <para>IUmbracoEntities can be retrieved with the IEntityService.</para>
/// <para>An IUmbracoEntity can participate in notifications.</para>
/// </remarks>
public interface IUmbracoEntity : ITreeEntity, IRememberBeingDirty
{
/// <summary>
/// Gets additional data for this entity.
/// </summary>
/// <remarks>Can be empty, but never null. To avoid allocating, do not
/// test for emptyness, but use <see cref="HasAdditionalData"/> instead.</remarks>
IDictionary<string, object> AdditionalData { get; }
/// <summary>
/// Determines whether this entity has additional data.
/// </summary>
/// <remarks>Use this property to check for additional data without
/// getting <see cref="AdditionalData"/>, to avoid allocating.</remarks>
bool HasAdditionalData { get; }
}
}

View File

@@ -0,0 +1,11 @@
namespace Umbraco.Core.Models.Entities
{
/// <summary>
/// Marker interface for value object, eg. objects without
/// the same kind of identity as an Entity (with its Id).
/// </summary>
public interface IValueObject
{
}
}

View File

@@ -0,0 +1,124 @@
using System;
using System.Reflection;
using System.Runtime.Serialization;
namespace Umbraco.Core.Models.Entities
{
/// <summary>
/// Provides a base class for tree entities.
/// </summary>
public abstract class TreeEntityBase : EntityBase, ITreeEntity
{
private static PropertySelectors _selectors;
private static PropertySelectors Selectors => _selectors ?? (_selectors = new PropertySelectors());
private string _name;
private int _creatorId;
private int _parentId;
private bool _hasParentId;
private ITreeEntity _parent;
private int _level;
private string _path;
private int _sortOrder;
private bool _trashed;
private class PropertySelectors
{
public readonly PropertyInfo Name = ExpressionHelper.GetPropertyInfo<ContentBase, string>(x => x.Name);
public readonly PropertyInfo CreatorId = ExpressionHelper.GetPropertyInfo<ContentBase, int>(x => x.CreatorId);
public readonly PropertyInfo ParentId = ExpressionHelper.GetPropertyInfo<ContentBase, int>(x => x.ParentId);
public readonly PropertyInfo Level = ExpressionHelper.GetPropertyInfo<ContentBase, int>(x => x.Level);
public readonly PropertyInfo Path = ExpressionHelper.GetPropertyInfo<ContentBase, string>(x => x.Path);
public readonly PropertyInfo SortOrder = ExpressionHelper.GetPropertyInfo<ContentBase, int>(x => x.SortOrder);
public readonly PropertyInfo Trashed = ExpressionHelper.GetPropertyInfo<ContentBase, bool>(x => x.Trashed);
}
// fixme
// ParentId, Path, Level and Trashed all should be consistent, and all derive from parentId, really
/// <inheritdoc />
[DataMember]
public string Name
{
get => _name;
set => SetPropertyValueAndDetectChanges(value, ref _name, Selectors.Name);
}
/// <inheritdoc />
[DataMember]
public int CreatorId
{
get => _creatorId;
set => SetPropertyValueAndDetectChanges(value, ref _creatorId, Selectors.CreatorId);
}
/// <inheritdoc />
[DataMember]
public int ParentId
{
get
{
if (_hasParentId) return _parentId;
if (_parent == null) throw new InvalidOperationException("Content does not have a parent.");
if (!_parent.HasIdentity) throw new InvalidOperationException("Content's parent does not have an identity.");
_parentId = _parent.Id;
if (_parentId == 0)
throw new Exception("Panic: parent has an identity but id is zero.");
_hasParentId = true;
_parent = null;
return _parentId;
}
set
{
if (value == 0)
throw new ArgumentException("Value cannot be zero.", nameof(value));
SetPropertyValueAndDetectChanges(value, ref _parentId, Selectors.ParentId);
_hasParentId = true;
_parent = null;
}
}
/// <inheritdoc />
public void SetParent(ITreeEntity parent)
{
_hasParentId = false;
_parent = parent;
OnPropertyChanged(Selectors.ParentId);
}
/// <inheritdoc />
[DataMember]
public int Level
{
get => _level;
set => SetPropertyValueAndDetectChanges(value, ref _level, Selectors.Level);
}
/// <inheritdoc />
[DataMember]
public string Path
{
get => _path;
set => SetPropertyValueAndDetectChanges(value, ref _path, Selectors.Path);
}
/// <inheritdoc />
[DataMember]
public int SortOrder
{
get => _sortOrder;
set => SetPropertyValueAndDetectChanges(value, ref _sortOrder, Selectors.SortOrder);
}
/// <inheritdoc />
[DataMember]
public bool Trashed
{
get => _trashed;
set => SetPropertyValueAndDetectChanges(value, ref _trashed, Selectors.Trashed);
}
}
}

View File

@@ -0,0 +1,18 @@
namespace Umbraco.Core.Models.Entities
{
/// <summary>
/// Represents the path of a tree entity.
/// </summary>
public class TreeEntityPath
{
/// <summary>
/// Gets or sets the identifier of the entity.
/// </summary>
public int Id { get; set; }
/// <summary>
/// Gets or sets the path of the entity.
/// </summary>
public string Path { get; set; }
}
}