using System; using System.Collections; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Data.Entity; using System.Globalization; using System.Linq; using System.Reflection; using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; using Tango.Core; using Tango.DAL.Remote.DB; using Tango.DAL.Remote; using Tango.Settings; using Tango.Core.Helpers; using System.ComponentModel.DataAnnotations.Schema; using System.ComponentModel.DataAnnotations; using System.Runtime.CompilerServices; using System.Data.Entity.Core.Objects; using Tango.Serialization; using System.Xml.Serialization; using Newtonsoft.Json; using Tango.Logging; using System.ComponentModel; using Tango.Core.Json; using Newtonsoft.Json.Converters; using Tango.BL.Serialization; using Newtonsoft.Json.Linq; using Tango.BL.ActionLogs; using Tango.BL.ValueObjects; using LiteDB; using System.Linq.Expressions; namespace Tango.BL { internal class ObservableEntitiesContainer { static ObservableEntitiesContainer() { RegisteredEntities = new List(); } /// /// Gets the list of all registered entities ever created. /// internal static List RegisteredEntities { get; private set; } } /// /// Represents a generic observable entity base class. /// /// /// /// [Serializable] public abstract class ObservableEntity : ExtendedObject, IObservableEntity, IActionLogComparable where T : class, IObservableEntity { #region Events /// /// Occurs after this observable has been modified and saved by this entity context or another. /// public event EventHandler Modified; #endregion #region Properties private Int32 _id; /// /// Gets or sets the entity identifier. /// [Column("ID")] [JsonIgnore] [BsonIgnore] [ParameterIgnore] [DatabaseGeneratedAttribute(DatabaseGeneratedOption.Identity)] public Int32 ID { get { return _id; } set { _id = value; RaisePropertyChanged(nameof(ID)); } } private String _guid; /// /// Gets or sets the entity unique identifier. /// [Key] [Column("GUID")] [ParameterIgnore] public String Guid { get { return _guid; } set { _guid = value; RaisePropertyChanged(nameof(Guid)); } } private DateTime _lastUpdated; /// /// Gets or sets the entity last updated data and time. /// [Column("LAST_UPDATED")] [ParameterIgnore] public DateTime LastUpdated { get { return _lastUpdated; } set { _lastUpdated = value; RaisePropertyChanged(nameof(LastUpdated)); } } private ReadOnlyObservableCollection _parameters; /// /// Gets a bind-able observable collection of the component properties. /// [NotMapped] [XmlIgnore] [BsonIgnore] [JsonIgnore] [ParameterIgnore] public ReadOnlyObservableCollection Parameters { get { if (_parameters == null) { _parameters = new ReadOnlyObservableCollection(this.CreateParametersCollection(ParameterItemMode.Binding)); } return _parameters; } private set { _parameters = value; RaisePropertyChangedAuto(); } } /// /// Gets or sets the entity last updated data and time. /// [ParameterIgnore] [JsonIgnore] [BsonIgnore] [NotMapped] public Type ObjectType { get { return GetType(); } } #endregion #region Constructors /// /// Initializes a new instance of the class. /// public ObservableEntity() { //ObservableEntitiesContainer.RegisteredEntities.Add(this); Guid = System.Guid.NewGuid().ToString(); LastUpdated = DateTime.UtcNow; } #endregion #region Public Methods /// /// Marks this entity as modified so all properties are saved. /// /// The context. public virtual void MarkModified(ObservablesContext context) { context.Entry(this).State = EntityState.Modified; } /// /// Saves the changes on this entity to database. /// public virtual void Save(ObservablesContext context) { if (context == ObservablesEntitiesAdapter.Instance.Context) { var tableName = this.GetType().GetCustomAttribute().Name; String propName = tableName.FromDalNameToTitleCase(); DbSet set = ObservablesEntitiesAdapter.Instance.Context.GetType().GetProperty(propName, BindingFlags.Instance | BindingFlags.Public).GetValue(context) as DbSet; ObservableCollection obs = ObservablesEntitiesAdapter.Instance.GetType().GetProperty(propName, BindingFlags.Instance | BindingFlags.Public).GetValue(ObservablesEntitiesAdapter.Instance) as ObservableCollection; if (!obs.Contains(this as T)) { set.Add(this as T); } ObservablesEntitiesAdapter.Instance.SaveChanges(); } else { context.SaveChanges(); } } /// /// Attaches this entity to the proper DbSet. /// public virtual void Attach(ObservablesContext context) { GetDbSet(context).Add(this as T); } /// /// Detaches this observable from the proper DbSet. /// public virtual void Detach(ObservablesContext context) { GetDbSet(context).Remove(this as T); } /// /// Saves the changes on this entity to database asynchronously. /// /// public Task SaveAsync(ObservablesContext context) { return Task.Factory.StartNew(() => { Save(context); }); } /// /// Reloads this observable entity. /// /// public Task Reload(ObservablesContext context) { return context.Entry(this).ReloadAsync(); } /// /// Gets the database set. /// /// The context. /// public DbSet GetDbSet(ObservablesContext context) { String tabelName = this.GetType().Name.PluralizeMVC(); var p = typeof(ObservablesContext).GetProperty(tabelName); if (p != null) { var set1 = p.GetValue(context) as DbSet; return set1; } else { tabelName = this.GetType().BaseType.Name.PluralizeMVC(); p = typeof(ObservablesContext).GetProperty(tabelName); if (p != null) { var set2 = p.GetValue(context) as DbSet; return set2; } } return null; } /// /// Gets the database set. /// /// The type of the 1. /// The context. /// public DbSet GetDbSet(ObservablesContext context) where T1 : class, IObservableEntity { return GetDbSet(context) as DbSet; } /// /// Clones this entity. /// /// public virtual T Clone() { return Clone(null); } public T Clone(Action action) { var cloned = Activator.CreateInstance(); foreach (var prop in typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance).Where(x => x.SetMethod != null)) { if (!prop.PropertyType.IsGenericTypeAndNotNullable()) { prop.SetValue(cloned, prop.GetValue(this)); } } cloned.ID = 0; cloned.Guid = System.Guid.NewGuid().ToString(); cloned.LastUpdated = DateTime.UtcNow; action?.Invoke(cloned); return cloned; } /// /// Compares another entity using a a JSON string comparison. /// /// The entity. /// public bool CompareUsingJson(T entity) { String me = JsonConvert.SerializeObject(this); String other = JsonConvert.SerializeObject(entity); return me == other; } /// /// Deletes this entity using an SQL statement which will cause the database delete cascade effect. /// /// /// public Task DeleteCascadeAsync(ObservablesContext context) { return context.Database.ExecuteSqlCommandAsync(String.Format("DELETE FROM {0} WHERE GUID='{1}'", this.GetType().GetCustomAttribute().Name, Guid)); } /// /// Raises the event. /// /// /// /// public void RaiseModified(ObservablesContext context, IObservableEntity source, IObservableEntity target) { Modified?.Invoke(this, new ObservableModifiedEventArgs(context, source, target)); } public ObjectContext GetObjectContext() { return GetObjectContextFromEntity(this); } public DbContext GetDbContext() { return GetDbContextFromEntity(this); } /// /// Removes this entity and all dependent entities from the specified db context. /// /// The context. public virtual void Delete(ObservablesContext context) { } #endregion #region Serialization public virtual EntitySerializationStrategy GetDefaultSerializationStrategy(EntitySerializationFlags flags) { EntitySerializationStrategy st = new EntitySerializationStrategy(); st.Ignore(() => this.HasErrors); st.Ignore(() => this.Parameters); st.Ignore(() => this.DesignMode); st.Ignore(() => this.LogManager); st.Ignore(() => this.ObjectType); st.Ignore(() => this.TemporaryManager); st.Ignore(() => this.ValidateOnPropertyChanged); st.Ignore(() => this.LastUpdated); st.Ignore(() => this.ValidationErrors); st.Ignore(() => this.ID); st.Ignore(this.GetType().GetField("_entityWrapper")); if (flags.HasFlag(EntitySerializationFlags.IgnoreGuids)) { st.Ignore(() => this.Guid); } if (flags.HasFlag(EntitySerializationFlags.IgnoreReferenceTypes)) { var props = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance).Where(x => !x.PropertyType.IsPrimitive && !x.PropertyType.IsGenericType && !x.PropertyType.IsNullable() && x.PropertyType != typeof(byte[]) && x.PropertyType != typeof(String) && x.PropertyType != typeof(DateTime)).ToList(); foreach (var prop in props) { st.Ignore(prop); } } if (flags.HasFlag(EntitySerializationFlags.IgnoreCollections)) { var props = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance).Where(x => x.PropertyType.IsGenericTypeAndNotNullable()).ToList(); foreach (var prop in props) { st.Ignore(prop); } } foreach (var prop in typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance).Where(x => x.SetMethod == null)) { st.Ignore(prop); } return st; } public static T FromJson(String json, EntitySerializationStrategy serializationStrategy, EntitySerializationFlags flags) { var settings = new JsonSerializerSettings() { ContractResolver = new SerializableEntityContractResolver(serializationStrategy, flags), }; if (flags.HasFlag(EntitySerializationFlags.PreserveReferencesHandling)) { settings.PreserveReferencesHandling = PreserveReferencesHandling.All; } settings.Converters.Add(new StringEnumConverter { CamelCaseText = false }); return JsonConvert.DeserializeObject(json, settings); } public String ToJson(EntitySerializationStrategy serializationStrategy, EntitySerializationFlags flags) { var settings = new JsonSerializerSettings() { ContractResolver = new SerializableEntityContractResolver(serializationStrategy, flags), }; if (flags.HasFlag(EntitySerializationFlags.PreserveReferencesHandling)) { settings.PreserveReferencesHandling = PreserveReferencesHandling.All; } settings.Converters.Add(new StringEnumConverter { CamelCaseText = false }); String json = JsonConvert.SerializeObject(this, flags.HasFlag(EntitySerializationFlags.Indented) ? Formatting.Indented : Formatting.None, settings); return json; } public static T FromJson(String json) { return FromJson(json, new EntitySerializationStrategy(), EntitySerializationFlags.IgnoreReferenceTypes | EntitySerializationFlags.Indented); } public String ToJson() { return ToJson(new EntitySerializationStrategy(), EntitySerializationFlags.IgnoreReferenceTypes | EntitySerializationFlags.Indented); } public void PopulateFromJson(string json, EntitySerializationStrategy serializationStrategy, EntitySerializationFlags flags) { var settings = new JsonSerializerSettings() { ContractResolver = new SerializableEntityContractResolver(serializationStrategy, flags), }; if (flags.HasFlag(EntitySerializationFlags.PreserveReferencesHandling)) { settings.PreserveReferencesHandling = PreserveReferencesHandling.All; } settings.Converters.Add(new StringEnumConverter { CamelCaseText = false }); JsonConvert.PopulateObject(json, this, settings); } public void PopulateFromJson(string json) { PopulateFromJson(json, new EntitySerializationStrategy(), EntitySerializationFlags.PreserveReferencesHandling); } #endregion #region Private Methods private static DbContext GetDbContextFromEntity(object entity) { var object_context = GetObjectContextFromEntity(entity); if (object_context == null) return null; return new DbContext(object_context, dbContextOwnsObjectContext: false); } private static ObjectContext GetObjectContextFromEntity(object entity) { var field = entity.GetType().GetField("_entityWrapper"); if (field == null) return null; var wrapper = field.GetValue(entity); var property = wrapper.GetType().GetProperty("Context"); var context = (ObjectContext)property.GetValue(wrapper, null); return context; } #endregion #region Validation //Holds the current validation errors. private List> _currentErrors = new List>(); /// /// Occurs when the validation errors have changed for a property or for the entire entity. /// public event EventHandler ErrorsChanged; private ObservableCollection _validationErrors; /// /// Gets or sets the validation errors. /// [NotMapped] [ParameterIgnore] [XmlIgnore] [BsonIgnore] [JsonIgnore] public ObservableCollection ValidationErrors { get { return _validationErrors; } protected set { _validationErrors = value; RaisePropertyChangedAuto(); } } private bool _validateOnPropertyChanged; [NotMapped] [ParameterIgnore] [JsonIgnore] [BsonIgnore] [XmlIgnore] public bool ValidateOnPropertyChanged { get { return _validateOnPropertyChanged; } set { _validateOnPropertyChanged = value; RaisePropertyChangedAuto(); } } private bool _hasErrors; /// /// Gets a value that indicates whether the entity has validation errors. /// [NotMapped] [ParameterIgnore] [JsonIgnore] [BsonIgnore] [XmlIgnore] public bool HasErrors { get { return _hasErrors; } set { _hasErrors = value; RaisePropertyChangedAuto(); } } /// /// Performs entity field validation. /// Will throw an exception with the proper message if one of the fields is invalid. /// /// The context. public virtual bool Validate(ObservablesContext context) { HasErrors = false; _currentErrors.Clear(); if (ValidationErrors == null) { ValidationErrors = new ObservableCollection(); } ValidationErrors.Clear(); OnValidating(context); foreach (var prop in this.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance)) { foreach (var validation in prop.GetCustomAttributes()) { if (!validation.IsValid(prop.GetValue(this))) { _currentErrors.Add(new KeyValuePair(prop.Name, validation.ErrorMessage)); ValidationErrors.Add(validation.ErrorMessage); } } HasErrors = _currentErrors.Count > 0; RaiseError(prop.Name); } foreach (var error in _currentErrors) { ValidationErrors.Add(error.Value); } return !HasErrors; } /// /// Gets the validation errors for a specified property or for the entire entity. /// /// The name of the property to retrieve validation errors for; or null or , to retrieve entity-level errors. /// /// The validation errors for the property or entity. /// public IEnumerable GetErrors(string propertyName) { return _currentErrors.Where(x => x.Key == propertyName).Select(x => x.Value).ToList(); } /// /// Called when entity is validating. /// protected virtual void OnValidating(ObservablesContext context) { } /// /// Inserts the error. /// /// Name of the property. /// The error. protected void InsertError(String propName, String error) { _currentErrors.Add(new KeyValuePair(propName, error)); } /// /// Invoked the event. /// /// Name of the property. protected void RaiseError(String propName) { ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propName)); } #endregion #region Virtual Methods /// /// Called when before entity is saved by the context. /// public virtual void OnBeforeSave() { } #endregion #region INotify Property Changed /// /// Raises the property changed event. /// /// Name of the property. protected override void RaisePropertyChanged(string propName) { base.RaisePropertyChanged(propName); if (ValidateOnPropertyChanged) { Validate(null); } } public void RaisePropertyChanged(Expression> expression) { MemberExpression member = expression.Body as MemberExpression; if (member == null) throw new ArgumentException(string.Format( "Expression '{0}' refers to a method, not a property.", expression.ToString())); PropertyInfo propInfo = member.Member as PropertyInfo; if (propInfo == null) throw new ArgumentException(string.Format( "Expression '{0}' refers to a field, not a property.", expression.ToString())); RaisePropertyChanged(propInfo.Name); } #endregion #region Action Log /// /// Returns true if the specified property should be ignored during ActionLog comparison. /// /// Name of the property. /// bool IActionLogComparable.ShouldActionLogIgnore(string propName) { return propName == nameof(LastUpdated) || OnShouldActionLogIgnore(propName); } /// /// override to specified properties to be ignored when doing ActionLog comparison. /// /// Name of the property. /// protected virtual bool OnShouldActionLogIgnore(string propName) { return false; } /// /// Returns an optional custom name for this instance in the comparison tree. /// /// string IActionLogComparable.GetActionLogName() { return OnGetActionLogName(); } /// /// override to specified a custom name for this instance in the ActionLog comparison tree. /// /// protected virtual String OnGetActionLogName() { return this.GetType().Name; } #endregion } }