/// <summary> /// Build a dynamic record containing properties depending on the area and the profile /// </summary> /// <param name="record"></param> /// <param name="area"></param> /// <param name="userId"></param> /// <param name="profile"></param> /// <param name="request"></param> /// <returns>true if the record is filtered</returns> public bool FilterRecord(DSRecord record, string area, int userId, UserProfile.EUserProfile profile, DSRequest request) { foreach (KeyValuePair <string, DSTable> table in Tables) { if (table.Value.Table != record.GetType()) { continue; } return(table.Value.FilterRecord(record, area, userId, profile, request)); } return(false); }
/// <summary> /// Build a dynamic record containing properties depending on the area and the profile /// </summary> /// <param name="record"></param> /// <param name="area"></param> /// <param name="userId"></param> /// <param name="profile"></param> /// <returns></returns> public JObject FilterRecord(DSRecord record, string area, int userId, UserProfile.EUserProfile profile) { foreach (KeyValuePair <string, DSTable> table in Tables) { if (table.Value.Table != record.GetType()) { continue; } return(table.Value.FilterRecord(record, area, userId, profile)); } return(null); }
/// <summary> /// Update the cache within the list of records updated by the transaction /// </summary> /// <param name="records"></param> /// <param name="customerId"></param> /// <param name="tick"></param> public void UpdateCache(List <Tuple <string, DSRecord, InformationRecord> > records, int customerId, int tick) { if (!_enable) { return; } Lock(customerId); // lock critical section // Check if the customer is currently loaded if (!_tick.ContainsKey(customerId)) { Unlock(customerId); // unlock critical section return; } try { // Update the cache within the list of records from the transaction currently executed Dictionary <string, Dictionary <int, Tuple <DSRecord, InformationRecord> > > currentTable = _records[customerId]; foreach (Tuple <string, DSRecord, InformationRecord> record in records) { if (!currentTable.ContainsKey(record.Item1)) { continue; } currentTable[record.Item1][record.Item2.Id] = Tuple.Create(DSRecord.Copy(record.Item2), InformationRecord.Copy(record.Item3)); } _tick[customerId] = tick; if (IsDebug()) { Debug($"[{customerId}] Cache updated in tick {_tick[customerId]}"); } } catch (System.Exception ex) { Exception($"[{customerId}] Unable to update the cache", ex); } Unlock(customerId); // unlock critical section }
/// <summary> /// Retrieve a list of tuple (DSRecord, Table) attached to a given record (table, id) for the given profile /// This function is used to retrieve a list of records attached to the current update /// Example : /// The object A is not visible for the user Y /// The user X updates the object A /// The object A becomes visible for the user Y /// In that case, object A must be added and notified to the user Y /// This function builds this case /// </summary> /// <param name="cache"></param> /// <param name="table"></param> /// <param name="id"></param> /// <param name="customerId"></param> /// <param name="userId"></param> /// <param name="profile"></param> /// <param name="area"></param> /// <param name="deepUpdate"></param> /// <param name="recordAlreadyRead"></param> /// <param name="informationAlreadyRead"></param> /// <returns></returns> virtual public void GetListRecordsConcernedByUpdate(DSCache cache, string table, int id, int customerId, int userId, UserProfile.EUserProfile profile, string area, bool deepUpdate, DSRecord recordAlreadyRead, InformationRecord informationAlreadyRead) { if (id < 0 || profile != UserProfile.EUserProfile.None || cache.Is(table, id) != null) { return; } cache.Set(table, id, null); }
/// <summary> /// Retrieve the userId into the record /// </summary> /// <param name="record"></param> /// <returns></returns> public int?GetUserId(DSRecord record) { if (_field == null) { _field = record.GetType().GetProperty(FieldUserId); } if (_field == null) { return(null); } object value = _field.GetValue(record); if (value == null) { return(null); } return((int)value); }
/// <summary> /// This static function checks if the list of restrictions limits the access to the element /// on depends on the current area and the current profile /// </summary> /// <param name="allows"></param> /// <param name="area">Null, "Create", "Read", "Update" or "Delete"</param> /// <param name="profile"></param> /// <param name="action"></param> /// <param name="userId"></param> /// <param name="record"></param> public static bool IsAllowed(IEnumerable <DSAllowAttribute> allows, string area, UserProfile.EUserProfile profile, string action, int userId, DSRecord record) { // everything is allowed ? if (!allows.Any()) { return(true); } if (area == null && profile == UserProfile.EUserProfile.None) { return(true); } // if the record is null, not allowed! if (record == null) { return(false); } foreach (DSAllowAttribute allow in allows) { // By default: Nobody can access to this table if (allow.Area == null && allow.Profile == UserProfile.EUserProfile.None && allow.Action == null) { continue; } // Check area (if restriction.Area = "*" : allow all areas) if (allow.Area != null && !allow.Area.Equals("*") && area != null && !allow.Area.Equals(area)) { continue; } // Check profile if (allow.Profile != UserProfile.EUserProfile.None && profile != UserProfile.EUserProfile.None && !UserProfile.IsInRole(profile, allow.Profile)) { continue; } // Check action if (allow.Action != null && !allow.Action.Equals("*") && action != null && !allow.Action.Equals(action)) { continue; } // retrieve the field if it doesn't exist if (allow.FieldUserId != null) { int?currentUserId = allow.GetUserId(record); if (currentUserId == null || currentUserId.Value != userId) { continue; } } return(true); } return(false); }
/// <summary> /// This function is called after deleting the record ... used to complete the deletion /// </summary> /// <param name="database"></param> /// <param name="tick"></param> /// <param name="table"></param> /// <param name="record"></param> public void OnAfterDeleteRecord(Common.Database.DatabaseContext database, int tick, string table, DSRecord record) { }
/// <summary> /// This function is called after updating the record ... used to complete the update /// </summary> /// <param name="database"></param> /// <param name="tick"></param> /// <param name="table"></param> /// <param name="record"></param> public void OnAfterUpdateRecord(Common.Database.DatabaseContext database, int tick, string table, DSRecord record) { if (table.Equals("Language")) { if (database is Module.Administration.DatabaseContext administration && record is DSRecordWithCustomerId language) { string tickKey = $"Language.Tick.{language.CustomerId}"; ParameterRecord parameter = database._Parameter.SingleOrDefault(e => e.Key.Equals(tickKey)); if (parameter != null) { parameter.Value = tick.ToString(); } // Unable to add a line into "_Parameter" due to the lock set into the table "_Parameter"! } } }
/// <summary> /// This function is called after creating the record ... used to complete the creation /// </summary> /// <param name="database"></param> /// <param name="tick"></param> /// <param name="table"></param> /// <param name="record"></param> public void OnAfterCreateRecord(Common.Database.DatabaseContext database, int tick, string table, DSRecord record) { if (table.Equals("User")) { if (record is DSRecordWithCustomerId user && database is Module.Administration.DatabaseContext administration) { // Create the first notification or reports ... administration.Notification.Add(new NotificationRecord() { UserId = user.Id, CustomerId = user.CustomerId, LastTick = tick, Date = DateTime.Now, Report = NotificationRecord.NOTIFICATION }); administration.SaveChanges(); } } /* TODO : Handle AttachedFile * else if (table.Equals("AttachedFile")) * { * if (record is AttachedFileRecord attachment && * database is Module.Stock.DatabaseContext stock) * { * // Load the file, save it into the table and delete it * * string filename = System.Web.HttpContext.Current.Server.MapPath($"~/App_Data/{attachment.FileId:D6}_{attachment.Filename}"); * Debug($"Loading file {filename} into the database {attachment.ToString()} ..."); * * AttachedFileContentRecord content = null; * if (System.IO.File.Exists(filename)) * { * try * { * content = new AttachedFileContentRecord() { Content = System.IO.File.ReadAllBytes(filename), CustomerId = attachment.CustomerId }; * stock.AttachedFileContent.Add(content); * stock.SaveChanges(); * * // update the fileId * * attachment.FileId = content.Id; * stock.SaveChanges(); * } * catch (System.Exception ex) * { * Exception($"Unable to load the file {filename}", ex); * return; * } * attachment.Size = content == null || content.Content == null ? 0 : content.Content.Length; * } * else * { * Warn($"The file {filename} doesn't exist ... The file may be already saved into the database!"); * attachment.Date = DateTime.Now; * } * * try * { * System.IO.File.Delete(filename); * } * catch (System.Exception) * { * Warn($"Unable to delete the file {filename}"); * } * * Info($"File {filename} loaded into the database"); * } * } */ }
/// <summary> /// Retrieve a list of tuple (DSRecord, Table) attached to a given record (table, id) for the given profile /// </summary> /// <param name="cache"></param> /// <param name="table"></param> /// <param name="id"></param> /// <param name="customerId"></param> /// <param name="userId"></param> /// <param name="profile"></param> /// <param name="area"></param> /// <param name="deepUpdate"></param> /// <param name="recordAlreadyRead"></param> /// <param name="informationAlreadyRead"></param> /// <returns></returns> public override void GetListRecordsConcernedByUpdate(DSCache cache, string table, int id, int customerId, int userId, UserProfile.EUserProfile profile, string area, bool deepUpdate, DSRecord recordAlreadyRead, InformationRecord informationAlreadyRead) { if (id < 0 || profile == UserProfile.EUserProfile.None || cache.Is(table, id) != null) { return; } // Retrieve the current record DSRecord currentRecord = null; Tuple <DSRecord, InformationRecord> currentRecordFromCache = cache.GetRecord(this, table, id); if (currentRecordFromCache == null && recordAlreadyRead == null) { switch (table) { case "Customer": currentRecord = Customer.Find(id); break; case "Language": currentRecord = Language.Find(id); break; case "User": currentRecord = User.Find(id); break; case "Module": currentRecord = Module.Find(id); break; case "UserModule": currentRecord = UserModule.Find(id); break; default: base.GetListRecordsConcernedByUpdate(cache, table, id, customerId, userId, profile, area, deepUpdate, recordAlreadyRead, informationAlreadyRead); return; } if (currentRecord == null) { cache.Set(table, id, null); return; } currentRecordFromCache = cache.SetRecord(this, table, id, currentRecord, null); } else if (currentRecordFromCache == null && recordAlreadyRead != null) { currentRecordFromCache = cache.SetRecord(this, table, id, recordAlreadyRead, informationAlreadyRead); } if (currentRecordFromCache != null) { currentRecord = currentRecordFromCache.Item1; } // Check if the record is concerned by the current update if (currentRecord is DSRecordWithCustomerId currentCustomerRecord) { if (currentCustomerRecord.CustomerId != customerId) { cache.Set(table, id, null); return; } } if (currentRecord is CustomerRecord customer) { cache.Set(table, id, customer.Id != customerId ? null : currentRecord); return; } if (currentRecord as LanguageRecord != null || currentRecord as ModuleRecord != null || currentRecord as UserModuleRecord != null || currentRecord as UserRecord != null) { cache.Set(table, id, currentRecord); return; } base.GetListRecordsConcernedByUpdate(cache, table, id, customerId, userId, profile, area, deepUpdate, recordAlreadyRead, informationAlreadyRead); }
/// <summary> /// This function is called after deleting the record ... used to complete the deletion /// </summary> /// <param name="database"></param> /// <param name="tick"></param> /// <param name="table"></param> /// <param name="record"></param> public void OnAfterDeleteRecord(DatabaseContext database, int tick, string table, DSRecord record) { if (_request == null) { return; } _request.OnAfterDeleteRecord(database, tick, table, record); }
/// <summary> /// Execute a request on the database schema (Create, Update or Delete a record for an existing table) /// on depends on the restriction view for the area and the profile /// </summary> /// <param name="database"></param> /// <param name="tick"></param> /// <param name="customerId"></param> /// <param name="userId"></param> /// <param name="area"></param> /// <param name="profile"></param> /// <param name="table"></param> /// <param name="action"></param> /// <param name="id"></param> /// <param name="record"></param> /// <returns></returns> public void OnAfterExecuteRequest(DatabaseContext database, int tick, int customerId, int userId, string area, UserProfile.EUserProfile profile, string table, string action, int id, DSRecord record) { if (!Tables.ContainsKey(table)) { throw new ExceptionDefinitionRecord("ERR_REQUEST_UNKNOWN"); } Tables[table].OnAfterExecuteRequest(database, tick, customerId, userId, area, profile, action, id, record); }
/// <summary> /// Execute a list of requests from a client /// </summary> /// <param name="transaction"></param> /// <returns>RequestId, Error, Record</returns> public List <Tuple <DSRecord, InformationRecord> > ExecuteTransaction(DSTransaction transaction) { Info($"Executing the transaction [{transaction.RequestId}] containing {transaction.Requests.Count} requests ..."); List <Tuple <DSRecord, InformationRecord> > recordsTreated = new List <Tuple <DSRecord, InformationRecord> >(); List <Tuple <string, DSRecord, InformationRecord> > recordsToUpdate = new List <Tuple <string, DSRecord, InformationRecord> >(); // Retrieve the database schema DSDatabase schema = ConfigurationManager.Schemas[transaction.Area]; if (schema == null) { Error("No schema available!"); throw new ExceptionDefinitionRecord("ERR_SCHEMA"); } // Lock database during the execution of the request using (DatabaseLock lockDatabase = Database.Lock(transaction.CustomerId)) { try { if (IsVerbose()) { Verbose("Getting the first tick of the transaction ..."); } // Get the tick string tickKey = $"Database.Tick.{transaction.CustomerId}"; int tick = 0; ParameterRecord tickRecord = Database._Parameter.FirstOrDefault(p => p.Key.Equals(tickKey)); if (tickRecord == null) { tickRecord = new ParameterRecord { Key = tickKey, Value = transaction.Requests.Count.ToString() }; tickRecord = Database._Parameter.Add(tickRecord); } else { tick = int.Parse(tickRecord.Value); tickRecord.Value = (tick + transaction.Requests.Count).ToString(); } Database.SaveChanges(); if (IsDebug()) { Debug($"First tick is {tick}"); } // Execute the OnBefore trigger if (IsVerbose()) { Verbose("Executing the pre-request of the transaction ..."); } transaction.SetNewTick(tick); foreach (DSRequest request in transaction.Requests) { // Execute the request if (IsVerboseAll()) { Verbose($"Executing the pre-request[{request.Id}] with tick[{request.NewTick}]: {request} ..."); } // Execute the trigger before requesting schema.OnBeforeExecuteRequest(Database, request.NewTick, transaction.CustomerId, transaction.UserId, transaction.Area, transaction.Profile, request.Table, request.Action, request.RecordId, request.Record, request.Identity); } if (IsDebug()) { Debug($"Pre-request executed for {transaction.Requests.Count} requests"); } // Execute each request if (IsVerbose()) { Verbose("Executing the transaction ..."); } // Execution by lot List <RequestTableRecord> actions = new List <RequestTableRecord>(); int index = 0; foreach (List <DSRequest> lot in transaction.LotRequests) { // Execute the lot of requests if (IsVerbose()) { Verbose($"Executing the lot[{index}] with {lot.Count} requests ..."); } // Execute the lot of requests recordsTreated.AddRange(schema.ExecuteRequest(Database, transaction, lot)); // Saving data Database.SaveChanges(); index++; } if (IsVerbose()) { Verbose("Building the list of actions ..."); } index = 0; foreach (DSRequest request in transaction.Requests) { Tuple <DSRecord, InformationRecord> recordTreated = recordsTreated[index]; // Keep in memory all records executed recordsToUpdate.Add(Tuple.Create(request.Table, recordTreated.Item1, recordTreated.Item2)); // The request is correctly executed ... Store a new request actions.Add(new RequestTableRecord { Tick = request.NewTick, CustomerId = transaction.CustomerId, UserId = transaction.UserId, RequestId = transaction.RequestId, Table = request.Table, Action = request.Action, Id = (recordTreated == null || recordTreated.Item1 == null ? -1 : recordTreated.Item1.Id) }); Info($"The request[{request.Id}] has correctly been executed : {(recordTreated == null ? "null" : recordTreated.ToString())}"); index++; } if (IsDebug()) { Debug($"Transaction executed with {transaction.Requests.Count} requests"); } // Write actions into the RequestTable if (IsVerbose()) { Verbose($"Writing {actions.Count} actions into the RequestTable ..."); } Database._RequestTable.AddRange(actions); Database.SaveChanges(); if (IsDebug()) { Debug($"{actions.Count} actions written into the RequestTable"); } // Execute the OnAfter trigger if (IsVerbose()) { Verbose("Executing the post-request of the transaction ..."); } foreach (DSRequest request in transaction.Requests) { // Execute the request if (IsVerboseAll()) { Verbose($"Executing the post-request[{request.Id}] with tick[{request.NewTick}]: {request} ..."); } // Execute the trigger before requesting DSRecord record = recordsTreated[request.Id].Item1; if (record != null) { schema.OnAfterExecuteRequest(Database, request.NewTick, transaction.CustomerId, transaction.UserId, transaction.Area, transaction.Profile, request.Table, request.Action, record.Id, record); } } if (IsDebug()) { Debug($"Post-request executed for {transaction.Requests.Count} requests"); } // Unlock the database and commit all changes if (IsVerbose()) { Verbose("Committing changes ..."); } lockDatabase.Commit(); if (IsVerbose()) { Verbose("Changes committed"); } // Update the cache manager if (IsVerbose()) { Verbose("Updating cache ..."); } DatabaseCacheManager.Instance.UpdateCache(recordsToUpdate, transaction.CustomerId, tick + transaction.Requests.Count); if (IsVerbose()) { Verbose("Cache updated"); } } catch (System.Exception ex) { Exception("An exception occurs on executing the transaction", ex); bool saveFailed = false; do { saveFailed = false; try { Database.SaveChanges(); } catch (DbUpdateException ex2) { saveFailed = true; ex2.Entries.Single().Reload(); } }while (saveFailed); // Rollback all request already executed throw ex; } } Info("Transaction done"); return(recordsTreated); }
/// <summary> /// Load all labels if it's necessary /// </summary> /// <param name="database"></param> /// <param name="customerId"></param> private void Load(DatabaseContext database, int?customerId) { int lastUpdate = 0; _mutex.Wait(); // lock critical section if (customerId == null) { // Load all tickId for each customer or create it if it doesn't exist foreach (CustomerRecord customer in database.Customer.ToList()) { int tick = 0; // Retrieve the tick of the database string tickKey = $"Database.Tick.{customer.Id}"; ParameterRecord parameter = database._Parameter.FirstOrDefault(e => e.Key.Equals(tickKey)); if (parameter != null) { int.TryParse(parameter.Value, out tick); } // Update or retrieve the tick of the language tickKey = $"Language.Tick.{customer.Id}"; parameter = database._Parameter.FirstOrDefault(e => e.Key.Equals(tickKey)); if (parameter == null) { database._Parameter.Add(new ParameterRecord() { Key = tickKey, Value = tick.ToString() }); } else { parameter.Value = tick.ToString(); } _lastUpdate[customer.Id] = tick; } database.SaveChanges(); } else { string tickKey = $"Language.Tick.{customerId.Value}"; ParameterRecord parameter = database._Parameter.SingleOrDefault(e => e.Key.Equals(tickKey)); if (parameter == null) { Warn($"Please, create the parameter '{tickKey}' into the database to avoid loading labels every time!"); } if (parameter != null && !String.IsNullOrWhiteSpace(parameter.Value) && !int.TryParse(parameter.Value, out lastUpdate)) { lastUpdate = 0; } if (parameter != null && _lastUpdate.ContainsKey(customerId.Value) && lastUpdate == _lastUpdate[customerId.Value] && lastUpdate >= 0) { // no changes ... _mutex.Release(); // unlock critical section return; } // Update the language tick if (parameter != null && lastUpdate < 0) { string dbTickKey = $"Database.Tick.{customerId.Value}"; ParameterRecord parameterTick = database._Parameter.FirstOrDefault(e => e.Key.Equals(dbTickKey)); if (parameterTick != null) { parameter.Value = parameterTick.Value; database.SaveChanges(); } } } if (customerId == null) { Info("Loading all labels ..."); try { int i = 0; Dictionary <int, List <LanguageRecord> > newLabels = new Dictionary <int, List <LanguageRecord> >(); foreach (LanguageRecord label in database.Language.ToList()) { if (!newLabels.ContainsKey(label.CustomerId)) { newLabels[label.CustomerId] = new List <LanguageRecord>(); } newLabels[label.CustomerId].Add(DSRecord.Copy(label) as LanguageRecord); if (IsDebug() && customerId == null && label.CustomerId == 1) { Debug($"{label.Key.Trim()} = {label}"); } i++; } Info($"{i} labels loaded"); _labels = newLabels; } catch (Exception ex) { Exception("Unable to load all labels", ex); } } else { Info($"Loading labels because they recently change ('{lastUpdate}') for the customer '{customerId.Value}' ..."); try { int i = 0; List <LanguageRecord> newLabels = new List <LanguageRecord>(); foreach (LanguageRecord label in database.Language.Where(l => l.CustomerId == customerId.Value).ToList()) { newLabels.Add(DSRecord.Copy(label) as LanguageRecord); i++; } Info($"{i} labels loaded"); _labels[customerId.Value] = newLabels; _lastUpdate[customerId.Value] = lastUpdate; } catch (Exception ex) { Exception($"Unable to load labels for the customer '{customerId.Value}'", ex); } } _mutex.Release(); // unlock critical section if (customerId != null) { DatabaseCacheManager.Instance.Reload(database, customerId.Value, "Language"); } return; }