using System.Collections.Concurrent; using System.Reflection; using System.Text.RegularExpressions; using InABox.Core; namespace InABox.Database { public static class UserTrackingCache { public static ConcurrentBag Cache = new(); public static DateTime Date = DateTime.MinValue; } // public interface ICacheStore // { // void LoadCache(); // T GetCacheItem(); // void UpdateCache(); // } public class Store : IStore, IStore where T : Entity, new() { public bool IsSubStore { get; set; } public Guid UserGuid { get; set; } public string UserID { get; set; } public string Platform { get; set; } public string Version { get; set; } public IProvider Provider { get; set; } public virtual void Init() { } private Type GetTrackingType(Type type) { var attr = type.GetCustomAttribute(); if (attr == null) return type; if (!attr.Enabled) return null; if (attr.Parent != null) return GetTrackingType(attr.Parent); return type; } private void UpdateUserTracking(UserTrackingAction action) { if (IsSubStore) return; if (!DbFactory.IsSupported()) return; if (string.IsNullOrWhiteSpace(UserID)) return; var type = GetTrackingType(typeof(T)); if (type == null) return; if (UserTrackingCache.Date != DateTime.Today) { var tdata = Provider.Query( new Filter(x => x.Date).IsEqualTo(DateTime.Today), new Columns().Default(ColumnType.IncludeForeignKeys) ); UserTrackingCache.Cache = new ConcurrentBag(tdata.Rows.Select(x => x.ToObject())); UserTrackingCache.Date = DateTime.Today; } var tracking = UserTrackingCache.Cache.FirstOrDefault(x => Equals(x.User.ID, UserGuid) && DateTime.Equals(x.Date, DateTime.Today) && string.Equals(x.Type, type.Name)); if (tracking == null) { tracking = new UserTracking(); tracking.Date = DateTime.Today; tracking.Type = type.Name; tracking.User.ID = UserGuid; UserTrackingCache.Cache.Add(tracking); } tracking.Increment(DateTime.Now, action); Provider.Save(tracking); } protected IStore FindSubStore(Type t) { var defType = typeof(Store<>).MakeGenericType(t); var subType = DbFactory.Stores.Where(myType => myType.IsSubclassOf(defType)).FirstOrDefault(); var result = (IStore)Activator.CreateInstance(subType == null ? defType : subType); result.UserGuid = UserGuid; result.UserID = UserID; result.Platform = Platform; result.Version = Version; result.IsSubStore = true; result.Provider = Provider; return result; } protected IStore FindSubStore() where TEntity : Entity, new() { var defType = typeof(Store<>).MakeGenericType(typeof(TEntity)); var subType = DbFactory.Stores.Where(myType => myType.IsSubclassOf(defType)).FirstOrDefault(); var store = (Store)Activator.CreateInstance(subType == null ? defType : subType); store.UserGuid = UserGuid; store.UserID = UserID; store.Platform = Platform; store.Version = Version; store.IsSubStore = true; store.Provider = Provider; return store; } private Filter? RunScript(ScriptType type, Filter? filter) { var scriptname = type.ToString(); var key = string.Format("{0} {1}", typeof(T).EntityName(), scriptname); if (DbFactory.LoadedScripts.ContainsKey(key)) { var script = DbFactory.LoadedScripts[key]; Logger.Send(LogType.Information, UserID, string.Format("{0}.{1} Executing..", typeof(T).EntityName(), scriptname)); try { script.SetValue("Store", this); script.SetValue("Filter", filter); var result = script.Execute(); Logger.Send(LogType.Information, UserID, string.Format("{0}.{1} returns {2}", typeof(T).EntityName(), scriptname, result)); return result ? script.GetValue("Filter") as Filter : filter; } catch (Exception eExec) { Logger.Send(LogType.Information, UserID, string.Format("{0}.{1} Invoke Exception: {2}", typeof(T).EntityName(), scriptname, eExec.Message)); } } return filter; } private IEnumerable RunScript(ScriptType type, IEnumerable entities) { var scriptname = type.ToString(); var variable = typeof(T).EntityName().Split('.').Last() + "s"; var key = string.Format("{0} {1}", typeof(T).EntityName(), scriptname); if (DbFactory.LoadedScripts.ContainsKey(key)) { var script = DbFactory.LoadedScripts[key]; script.SetValue("Store", this); script.SetValue(variable, entities); Logger.Send(LogType.Information, UserID, string.Format("{0}.{1} Executing..", typeof(T).EntityName(), scriptname)); foreach (var entity in entities) try { var result = script.Execute(); Logger.Send(LogType.Information, UserID, string.Format("{0}.{1} returns {2}: {3}", typeof(T).EntityName(), scriptname, result, entity)); return result ? script.GetValue(variable) as IEnumerable : entities; } catch (Exception eExec) { var stack = new List(); var eStack = eExec; while (eStack != null) { stack.Add(eStack.Message); eStack = eStack.InnerException; } stack.Reverse(); var message = string.Join("\n", stack); Logger.Send(LogType.Information, UserID, string.Format("{0}.{1} Invoke Exception: {2}", typeof(T).EntityName(), scriptname, message)); } } return entities; } private CoreTable RunScript(ScriptType type, CoreTable table) { var scriptname = type.ToString(); var variable = typeof(T).EntityName().Split('.').Last() + "s"; var key = string.Format("{0} {1}", typeof(T).EntityName(), scriptname); if (DbFactory.LoadedScripts.ContainsKey(key)) { var script = DbFactory.LoadedScripts[key]; Logger.Send(LogType.Information, UserID, string.Format("{0}.{1} Executing..", typeof(T).EntityName(), scriptname)); try { script.SetValue("Store", this); script.SetValue(variable, table); var result = script.Execute(); Logger.Send(LogType.Information, UserID, string.Format("{0}.{1} returns {2}: {3}", typeof(T).EntityName(), scriptname, result, table)); return result ? script.GetValue(variable) as CoreTable : table; } catch (Exception eExec) { Logger.Send(LogType.Information, UserID, string.Format("{0}.{1} Invoke Exception: {2}", typeof(T).EntityName(), scriptname, eExec.Message)); } } return table; } //#region Session / Transaction Handling //void //OpenSession(String action, bool write) //{ // if (!IsSubStore) // Provider.OpenSession(action, write); //} //void //CloseSession(String action, bool write) //{ // if (!IsSubStore) // Provider.CloseSession(action, write); //} //#endregion #region List Functions private IEnumerable DoList(Filter? filter = null, Columns? columns = null, SortOrder? sort = null) { UpdateUserTracking(UserTrackingAction.Read); //last = DateTime.Now; //OpenSession("List", false); //LogStep("OpenSession"); try { var flt = PrepareFilter(filter); flt = RunScript(ScriptType.BeforeQuery, flt); var result = Provider.List(flt, columns, sort); //LogStep("PopulateTable"); AfterList(result); //CloseSession("List", false); //LogStep("CloseSession"); return result; } catch (Exception e) { //CloseSession("List", false); throw new Exception(e.Message + "\n\n" + e.StackTrace + "\n"); } } public IEnumerable List(Filter? filter = null, Columns? columns = null, SortOrder? sort = null) { return DoList(filter, columns, sort); } public IEnumerable List(Filter? filter = null, Columns? columns = null, SortOrder? sort = null) { return DoList((Filter?)filter, (Columns)columns, (SortOrder)sort); } protected virtual void AfterList(IEnumerable data) { } #endregion #region Query Functions protected virtual CoreTable OnQuery(Filter? filter, Columns? columns, SortOrder? sort) { return Provider.Query(filter, columns, sort); } private CoreTable DoQuery(Filter? filter = null, Columns? columns = null, SortOrder? sort = null) { UpdateUserTracking(UserTrackingAction.Read); //last = DateTime.Now; //OpenSession("Query", false); //LogStep("OpenSession"); try { var flt = PrepareFilter(filter); flt = RunScript(ScriptType.BeforeQuery, flt); var result = OnQuery(filter, columns, sort); //LogStep("PopulateTable"); AfterQuery(result); result = RunScript(ScriptType.AfterQuery, result); //CloseSession("Query", false); //LogStep("CloseSession"); return result; } catch (Exception e) { //CloseSession("Query", false); throw new Exception(e.Message + "\n\n" + e.StackTrace + "\n"); } } public CoreTable Query(Filter? filter = null, Columns? columns = null, SortOrder? sort = null) { return DoQuery(filter, columns, sort); } public CoreTable Query(Filter? filter = null, Columns? columns = null, SortOrder? sort = null) { return DoQuery((Filter?)filter, (Columns)columns, (SortOrder)sort); } protected virtual void AfterQuery(CoreTable data) { } #endregion #region Load Functions protected virtual Filter? PrepareFilter(Filter? filter) { return filter; } private T[] DoLoad(Filter? filter = null, SortOrder? sort = null) { UpdateUserTracking(UserTrackingAction.Read); //OpenSession("Load", false); T[] results = null; try { var flt = PrepareFilter(filter); flt = RunScript(ScriptType.BeforeQuery, flt); results = Provider.Load(flt, sort); AfterLoad(results); //CloseSession("Load", false); results = RunScript(ScriptType.AfterLoad, results) as T[]; return results; } catch (Exception e) { //CloseSession("Load", false); throw new Exception(e.Message + "\n\n" + e.StackTrace + "\n"); } } public Entity[] Load(Filter? filter = null, SortOrder? sort = null) { return DoLoad((Filter?)filter, sort != null ? (SortOrder)sort : null); } public T[] Load(Filter? filter = null, SortOrder? sort = null) { return DoLoad(filter, sort); } protected virtual void AfterLoad(IEnumerable items) { foreach (var item in items) item.SetObserving(true); } #endregion #region Saving Functions private static readonly Regex IsNumeric = new(@"^\d+$"); private void CheckAutoIncrement(params T[] entities) { if (ProcessNumericAutoInc(entities)) return; ProcessStringAutoIncrement(entities); } public static string AutoIncrementPrefix { get; set; } private bool ProcessStringAutoIncrement(params T[] entities) { if (!entities.Any()) return false; var autoinc = entities.First() as IStringAutoIncrement; if (autoinc != null) { var prop = CoreUtils.GetPropertyFromExpression(autoinc.AutoIncrementField()); var bRequired = false; foreach (var entity in entities) bRequired = bRequired || string.IsNullOrWhiteSpace(prop.GetValue(entity) as string); if (bRequired) { var filter = new Filter(prop.Name).IsGreaterThanOrEqualTo(String.Format("{0}0", AutoIncrementPrefix)) .And(prop.Name).IsLessThanOrEqualTo(String.Format("{0}9999999999", AutoIncrementPrefix)); var filter2 = autoinc.AutoIncrementFilter(); if (filter2 != null) filter = filter.And(filter2); // if (!string.IsNullOrWhiteSpace(AutoIncrementPrefix)) // { // var prefixfilter = new Filter(prop.Name).BeginsWith(AutoIncrementPrefix); // filter = filter == null ? prefixfilter : filter.And(prefixfilter); // } var newvalue = 0; var row = Provider.Query( filter, new Columns(new[] { prop.Name }), new SortOrder(prop.Name, SortDirection.Descending), 1 ).Rows.FirstOrDefault(); if (row != null) { var id = row.Get(prop.Name); if (!string.IsNullOrWhiteSpace(AutoIncrementPrefix)) id = id.Substring(AutoIncrementPrefix.Length); id = new string(id.Where(c => char.IsDigit(c)).ToArray()); int.TryParse(id, out newvalue); } foreach (var entity in entities) if (string.IsNullOrWhiteSpace(prop.GetValue(entity) as string)) { newvalue++; prop.SetValue(entity, AutoIncrementPrefix + string.Format(autoinc.AutoIncrementFormat(), newvalue)); } return true; } } return false; } private bool ProcessNumericAutoInc(params T[] entities) { if (!entities.Any()) return false; var autoinc = entities.First() as INumericAutoIncrement; if (autoinc != null) { var prop = CoreUtils.GetPropertyFromExpression(autoinc.AutoIncrementField()); var bRequired = false; foreach (var entity in entities) bRequired = bRequired || prop.GetValue(entity).Equals(0); if (bRequired) { var row = Provider.Query( autoinc.AutoIncrementFilter(), new Columns(new[] { prop.Name }), new SortOrder(prop.Name,SortDirection.Descending), 1 ).Rows.FirstOrDefault(); int newvalue = row != null ? row.Get(prop.Name) : 0; foreach (var entity in entities) { if (prop.GetValue(entity).Equals(0)) { newvalue++; prop.SetValue(entity, newvalue); } } return true; } } return false; } protected virtual void BeforeSave(T entity) { // Process any AutoIncrement Fields before we apply the Unique Code test // Thus, if we have a unique autoincrement, it will be populated prior to validation CheckAutoIncrement(entity); // Check for (a) blank code fields and (b) duplicate codes // There may be more than one code on an entity (why would you do this?) // so it's a bit trickier that I would hope Filter codes = null; var columns = new Columns(x => x.ID); var props = CoreUtils.PropertyList(typeof(T), x => x.GetCustomAttributes().Any(), true); foreach (var key in props.Keys) if (entity.HasOriginalValue(key) || entity.ID == Guid.Empty) { var code = CoreUtils.GetPropertyValue(entity, key) as string; if (string.IsNullOrWhiteSpace(code)) throw new NullCodeException(typeof(T), key); var expr = CoreUtils.GetPropertyExpression(key); //CoreUtils.GetMemberExpression(typeof(T),key) codes = codes == null ? new Filter(expr).IsEqualTo(code) : codes.Or(expr).IsEqualTo(code); columns.Add(key); } if (codes != null) { var filter = new Filter(x => x.ID).IsNotEqualTo(entity.ID); filter = filter.And(codes); var others = Provider.Query(filter, columns); var duplicates = new Dictionary(); foreach (var row in others.Rows) foreach (var key in props.Keys) { var eval = CoreUtils.GetPropertyValue(entity, key); var cval = row.Get(key); if (Equals(eval, cval)) duplicates[key] = eval; } if (duplicates.Any()) throw new DuplicateCodeException(typeof(T), duplicates); } } protected virtual void AfterSave(T entity) { } protected virtual void OnSave(T entity, ref string auditnote) { CheckAutoIncrement(entity); Provider.Save(entity); } private void DoSave(T entity, string auditnote) { UpdateUserTracking(UserTrackingAction.Write); entity = RunScript(ScriptType.BeforeSave, new[] { entity }).First(); //OpenSession("Save", true); //try //{ var changes = entity.ChangedValues(); //UpdateInternalLinks(entity); BeforeSave(entity); //OpenSession("Save", true); try { OnSave(entity, ref auditnote); } catch (Exception e) { //CloseSession("Save", true); throw e; } //CloseSession("Save", true); if (DbFactory.IsSupported()) { var notes = new List(); if (!string.IsNullOrEmpty(auditnote)) notes.Add(auditnote); if (!string.IsNullOrEmpty(changes)) notes.Add(changes); if (notes.Any()) AuditTrail(entity, notes); } AfterSave(entity); //UpdateExternalLinks(entity, false); entity.CommitChanges(); //CloseSession("Save", false); entity = RunScript(ScriptType.AfterSave, new[] { entity }).First(); //} //catch (Exception e) //{ // //CloseSession("Save", false); // throw new Exception(e.Message + "\n\n" + e.StackTrace + "\n"); //} } protected void AuditTrail(IEnumerable entities, IEnumerable notes) { var updates = new List(); foreach (var entity in entities) { var audit = new AuditTrail { EntityID = entity.ID, Timestamp = DateTime.Now, User = UserID, Note = string.Join(": ", notes) }; updates.Add(audit); } Provider.Save(updates); } protected void AuditTrail(Entity entity, IEnumerable notes) { AuditTrail(new[] { entity }, notes); } public void Save(T entity, string auditnote) { DoSave(entity, auditnote); } public void Save(Entity entity, string auditnote) { var ent = (T)entity; DoSave(ent, auditnote); } public void Save(IEnumerable entities, string auditnote) { DoSave(entities, auditnote); } public void Save(IEnumerable entities, string auditnote) { var updates = new List(); foreach (var entity in entities) updates.Add((T)entity); DoSave(updates, auditnote); } protected virtual void OnSave(IEnumerable entities, ref string auditnote) { CheckAutoIncrement(entities.ToArray()); Provider.Save(entities); } private void DoSave(IEnumerable entities, string auditnote) { UpdateUserTracking(UserTrackingAction.Write); entities = RunScript(ScriptType.BeforeSave, entities.ToList()); //OpenSession("Save", true); //try //{ var changes = new Dictionary(); foreach (var entity in entities) { changes[entity] = entity.ChangedValues(); //UpdateInternalLinks(entity); BeforeSave(entity); } try { //OpenSession("Save", true); OnSave(entities, ref auditnote); //CloseSession("Save", true); } catch (Exception e) { //CloseSession("Save", true); throw e; } if (DbFactory.IsSupported()) { var audittrails = new List(); foreach (var entity in entities) { var notes = new List(); if (!string.IsNullOrEmpty(auditnote)) notes.Add(auditnote); if (changes.ContainsKey(entity) && !string.IsNullOrEmpty(changes[entity])) notes.Add(changes[entity]); if (notes.Any()) { var audit = new AuditTrail { EntityID = entity.ID, Timestamp = DateTime.Now, User = UserID, Note = string.Join(": ", notes) }; audittrails.Add(audit); //Provider.Save(audit); } } if (audittrails.Any()) Provider.Save(audittrails); } foreach (var entity in entities) { AfterSave(entity); //UpdateExternalLinks(entity, false); entity.CommitChanges(); } entities = RunScript(ScriptType.AfterSave, entities); //} //catch (Exception e) //{ // throw e; //} } #endregion #region Delete Functions protected virtual void BeforeDelete(T entity) { } protected virtual void OnDelete(T entity) { Provider.Delete(entity, UserID); } protected virtual void OnDelete(IEnumerable entities) { Provider.Delete(entities, UserID); } private void DoDelete(T entity, string auditnote) { UpdateUserTracking(UserTrackingAction.Write); entity = RunScript(ScriptType.BeforeDelete, new[] { entity }).First(); //OpenSession("Delete",false); try { BeforeDelete(entity); //OpenSession("Delete",true); try { OnDelete(entity); } catch (Exception e) { //CloseSession("Delete", true); throw e; } //CloseSession("Delete",true); AfterDelete(entity); //UpdateExternalLinks(entity, true); //CloseSession("Delete", false); entity = RunScript(ScriptType.AfterDelete, new[] { entity }).First(); } catch (Exception e) { //CloseSession("Delete", false); throw new Exception(e.Message + "\n\n" + e.StackTrace + "\n"); } } private void DoDelete(IEnumerable entities, string auditnote) { UpdateUserTracking(UserTrackingAction.Write); entities = RunScript(ScriptType.BeforeDelete, entities); //OpenSession("Delete", false); try { foreach (var entity in entities) BeforeDelete(entity); //OpenSession("Delete", true); try { OnDelete(entities); } catch (Exception e) { ////CloseSession("Delete", true); throw e; } ////CloseSession("Delete", true); foreach (var entity in entities) AfterDelete(entity); //UpdateExternalLinks(entity, true); ////CloseSession("Delete", false); entities = RunScript(ScriptType.AfterDelete, entities); } catch (Exception e) { ////CloseSession("Delete", false); throw new Exception(e.Message + "\n\n" + e.StackTrace + "\n"); } } public void Delete(T entity, string auditnote) { DoDelete(entity, auditnote); } public void Delete(Entity entity, string auditnote) { DoDelete((T)entity, auditnote); } public void Delete(IEnumerable entities, string auditnote) { DoDelete(entities, auditnote); } public void Delete(IEnumerable entities, string auditnote) { var updates = new List(); foreach (var entity in entities) updates.Add((T)entity); DoDelete(updates, auditnote); } protected virtual void AfterDelete(T entity) { } #endregion #region BulkUpdate Functions public void BulkUpdate(IEnumerable entities) { BulkUpdate((IEnumerable)entities); } public virtual void BulkUpdate(IEnumerable entities) { UpdateUserTracking(UserTrackingAction.Write); Provider.Save(entities); } #endregion } }