using System.Linq; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.JsonPatch; using Newtonsoft.Json.Linq; using EnumsNET; using AyaNova.Util; using AyaNova.Api.ControllerHelpers; using AyaNova.Models; using System.Collections.Generic; using AyaNova.DataList; namespace AyaNova.Biz { internal class WidgetBiz : BizObject, IJobObject { internal WidgetBiz(AyContext dbcontext, long currentUserId, long userLocaleId, AuthorizationRoles UserRoles) { ct = dbcontext; UserId = currentUserId; UserLocaleId = userLocaleId; CurrentUserRoles = UserRoles; BizType = AyaType.Widget; } internal static WidgetBiz GetBiz(AyContext ct, Microsoft.AspNetCore.Http.HttpContext httpContext) { return new WidgetBiz(ct, UserIdFromContext.Id(httpContext.Items), UserLocaleIdFromContext.Id(httpContext.Items), UserRolesFromContext.Roles(httpContext.Items)); } //Version for internal use internal static WidgetBiz GetBizInternal(AyContext ct) { return new WidgetBiz(ct, 1, ServerBootConfig.AYANOVA_DEFAULT_LANGUAGE_ID, AuthorizationRoles.BizAdminFull); } //////////////////////////////////////////////////////////////////////////////////////////////// //EXISTS internal async Task ExistsAsync(long id) { return await ct.Widget.AnyAsync(e => e.Id == id); } //////////////////////////////////////////////////////////////////////////////////////////////// /// GET /// /// internal async Task GetAsync(long fetchId, bool log = true) { //This is simple so nothing more here, but often will be copying to a different output object or some other ops var ret = await ct.Widget.SingleOrDefaultAsync(m => m.Id == fetchId); if (log && ret != null) { //Log EventLogProcessor.LogEventToDatabaseAndSaveEntireContext(new Event(UserId, fetchId, BizType, AyaEvent.Retrieved), ct); } return ret; } //////////////////////////////////////////////////////////////////////////////////////////////// //CREATE //route linked version for external api access internal async Task CreateAsync(Widget inObj) { Validate(inObj, null); if (HasErrors) return null; else { //do stuff with widget Widget outObj = inObj; //Test get serial id visible id number from generator outObj.Serial = ServerBootConfig.WIDGET_SERIAL.GetNext(); outObj.Tags = TagUtil.NormalizeTags(outObj.Tags); outObj.CustomFields = JsonUtil.CompactJson(outObj.CustomFields); //Save to db var TheContext = ct; if (TheContext == null) TheContext = ServiceProviderProvider.DBContext; //TEST SPEED IMPROVEMENTS //ORIGINALLY // await TheContext.Widget.AddAsync(outObj); // await TheContext.SaveChangesAsync(); //add syncronously and don't save but let the log save //THIS SAVED 1 SECOND OUT OF 22 for seeding 500 widgets TheContext.Widget.Add(outObj); //Handle child and associated items: EventLogProcessor.LogEventToDatabaseAndSaveEntireContext(new Event(UserId, outObj.Id, BizType, AyaEvent.Created), TheContext); //This takes 16 seconds out of 22 when seeding 500 widgets SearchIndex(outObj, true); //This takes 2 seconds out of 22 when seeding 500 widgets TagUtil.ProcessUpdateTagsInRepository(TheContext, outObj.Tags, null); return outObj; } } // //Internal version for seeding // internal Widget Create(AyContext TempContext, Widget inObj) // { // Validate(inObj, null); // if (HasErrors) // return null; // else // { // //do stuff with widget // Widget outObj = inObj; // //Test get serial id visible id number from generator // outObj.Serial = ServerBootConfig.WIDGET_SERIAL.GetNext(); // outObj.Tags = TagUtil.NormalizeTags(outObj.Tags); // outObj.CustomFields = JsonUtil.CompactJson(outObj.CustomFields); // TempContext.Widget.Add(outObj); // TempContext.SaveChanges(); // //Handle child and associated items: // EventLogProcessor.LogEventToDatabaseAndSaveEntireContext(new Event(UserId, outObj.Id, BizType, AyaEvent.Created), TempContext); // SearchIndex(outObj, true); // TagUtil.ProcessUpdateTagsInRepository(TempContext, outObj.Tags, null); // return outObj; // } // } //////////////////////////////////////////////////////////////////////////////////////////////// //DUPLICATE // internal async Task DuplicateAsync(Widget dbObj) { Widget outObj = new Widget(); CopyObject.Copy(dbObj, outObj); outObj.Name = Util.StringUtil.NameUniquify(outObj.Name, 255); outObj.Id = 0; outObj.ConcurrencyToken = 0; //Test get serial id visible id number from generator outObj.Serial = ServerBootConfig.WIDGET_SERIAL.GetNext(); await ct.Widget.AddAsync(outObj); await ct.SaveChangesAsync(); //Handle child and associated items: EventLogProcessor.LogEventToDatabaseAndSaveEntireContext(new Event(UserId, outObj.Id, BizType, AyaEvent.Created), ct); SearchIndex(outObj, true); TagUtil.ProcessUpdateTagsInRepository(ct, outObj.Tags, null); return outObj; } //////////////////////////////////////////////////////////////////////////////////////////////// //UPDATE // //put internal bool Put(Widget dbObj, Widget inObj) { //make a snapshot of the original for validation but update the original to preserve workflow Widget SnapshotOfOriginalDBObj = new Widget(); CopyObject.Copy(dbObj, SnapshotOfOriginalDBObj); //Replace the db object with the PUT object CopyObject.Copy(inObj, dbObj, "Id,Serial"); dbObj.Tags = TagUtil.NormalizeTags(dbObj.Tags); dbObj.CustomFields = JsonUtil.CompactJson(dbObj.CustomFields); //Set "original" value of concurrency token to input token //this will allow EF to check it out ct.Entry(dbObj).OriginalValues["ConcurrencyToken"] = inObj.ConcurrencyToken; Validate(dbObj, SnapshotOfOriginalDBObj); if (HasErrors) return false; //Log event and save context EventLogProcessor.LogEventToDatabaseAndSaveEntireContext(new Event(UserId, dbObj.Id, BizType, AyaEvent.Modified), ct); SearchIndex(dbObj, false); TagUtil.ProcessUpdateTagsInRepository(ct, dbObj.Tags, SnapshotOfOriginalDBObj.Tags); return true; } //patch internal bool Patch(Widget dbObj, JsonPatchDocument objectPatch, uint concurrencyToken) { //Validate Patch is allowed //Note: Id and Serial are all checked for and disallowed in the validate code by default if (!ValidateJsonPatch.Validate(this, objectPatch)) return false; //make a snapshot of the original for validation but update the original to preserve workflow Widget SnapshotOfOriginalDBObj = new Widget(); CopyObject.Copy(dbObj, SnapshotOfOriginalDBObj); //Do the patching objectPatch.ApplyTo(dbObj); dbObj.Tags = TagUtil.NormalizeTags(dbObj.Tags); dbObj.CustomFields = JsonUtil.CompactJson(dbObj.CustomFields); ct.Entry(dbObj).OriginalValues["ConcurrencyToken"] = concurrencyToken; Validate(dbObj, SnapshotOfOriginalDBObj); if (HasErrors) return false; //Log event and save context EventLogProcessor.LogEventToDatabaseAndSaveEntireContext(new Event(UserId, dbObj.Id, BizType, AyaEvent.Modified), ct); SearchIndex(dbObj, false); TagUtil.ProcessUpdateTagsInRepository(ct, dbObj.Tags, SnapshotOfOriginalDBObj.Tags); return true; } private void SearchIndex(Widget obj, bool isNew) { //SEARCH INDEXING var SearchParams = new Search.SearchIndexProcessObjectParameters(UserLocaleId, obj.Id, BizType, obj.Name); SearchParams.AddText(obj.Notes).AddText(obj.Name).AddText(obj.Serial).AddText(obj.Tags).AddCustomFields(obj.CustomFields); if (isNew) Search.ProcessNewObjectKeywords(SearchParams); else Search.ProcessUpdatedObjectKeywords(SearchParams); } //////////////////////////////////////////////////////////////////////////////////////////////// //DELETE // internal bool Delete(Widget dbObj) { //Determine if the object can be deleted, do the deletion tentatively //Probably also in here deal with tags and associated search text etc ValidateCanDelete(dbObj); if (HasErrors) return false; ct.Widget.Remove(dbObj); ct.SaveChanges(); //Log event and save context EventLogProcessor.DeleteObject(UserId, BizType, dbObj.Id, dbObj.Name, ct); ct.SaveChanges(); Search.ProcessDeletedObjectKeywords(dbObj.Id, BizType); TagUtil.ProcessDeleteTagsInRepository(ct, dbObj.Tags); return true; } //////////////////////////////////////////////////////////////////////////////////////////////// //VALIDATION // //Can save or update? private void Validate(Widget proposedObj, Widget currentObj) { //NOTE: In DB schema only name and serial are not nullable //run validation and biz rules bool isNew = currentObj == null; //Name required if (string.IsNullOrWhiteSpace(proposedObj.Name)) AddError(ApiErrorCode.VALIDATION_REQUIRED, "Name"); //Name must be less than 255 characters if (proposedObj.Name.Length > 255) AddError(ApiErrorCode.VALIDATION_LENGTH_EXCEEDED, "Name", "255 max"); //If name is otherwise OK, check that name is unique if (!PropertyHasErrors("Name")) { //Use Any command is efficient way to check existance, it doesn't return the record, just a true or false if (ct.Widget.Any(m => m.Name == proposedObj.Name && m.Id != proposedObj.Id)) { AddError(ApiErrorCode.VALIDATION_NOT_UNIQUE, "Name"); } } //Start date AND end date must both be null or both contain values if (proposedObj.StartDate == null && proposedObj.EndDate != null) AddError(ApiErrorCode.VALIDATION_REQUIRED, "StartDate"); if (proposedObj.StartDate != null && proposedObj.EndDate == null) AddError(ApiErrorCode.VALIDATION_REQUIRED, "EndDate"); //Start date before end date if (proposedObj.StartDate != null && proposedObj.EndDate != null) if (proposedObj.StartDate > proposedObj.EndDate) AddError(ApiErrorCode.VALIDATION_STARTDATE_AFTER_ENDDATE, "StartDate"); //Enum is valid value //Note: because of the way the flags work as powers of 2 and with the 1 being the first flag value, basically any value up to the "All" value will be valid //because you can make any number from 0 to all using any combination of the flags so the only thing that will fail is less than zero or greater than All if (!proposedObj.Roles.IsValid()) { AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "Roles"); } //Any form customizations to validate? var FormCustomization = ct.FormCustom.SingleOrDefault(x => x.FormKey == AyaFormFieldDefinitions.WIDGET_KEY); if (FormCustomization != null) { //Yeppers, do the validation, there are two, the custom fields and the regular fields that might be set to required //validate users choices for required non custom fields RequiredFieldsValidator.Validate(this, FormCustomization, proposedObj); //validate custom fields CustomFieldsValidator.Validate(this, FormCustomization, proposedObj.CustomFields); } return; } //Can delete? private void ValidateCanDelete(Widget inObj) { //whatever needs to be check to delete this object } //////////////////////////////////////////////////////////////////////////////////////////////// //JOB / OPERATIONS // public async Task HandleJobAsync(OpsJob job) { //Hand off the particular job to the corresponding processing code //NOTE: If this code throws an exception the caller (JobsBiz::ProcessJobsAsync) will automatically set the job to failed and log the exeption so //basically any error condition during job processing should throw up an exception if it can't be handled switch (job.JobType) { case JobType.TestWidgetJob: await ProcessTestJobAsync(job); break; default: throw new System.ArgumentOutOfRangeException($"WidgetBiz.HandleJob-> Invalid job type{job.JobType.ToString()}"); } } /// /// /// Handle the test job /// /// private async Task ProcessTestJobAsync(OpsJob job) { var sleepTime = 30 * 1000; //Simulate a long running job here JobsBiz.UpdateJobStatus(job.GId, JobStatus.Running, ct); JobsBiz.LogJob(job.GId, $"WidgetBiz::ProcessTestJob started, sleeping for {sleepTime} seconds...", ct); //Uncomment this to test if the job prevents other routes from running //result is NO it doesn't prevent other requests, so we are a-ok for now await Task.Delay(sleepTime); JobsBiz.LogJob(job.GId, "WidgetBiz::ProcessTestJob done sleeping setting job to finished", ct); JobsBiz.UpdateJobStatus(job.GId, JobStatus.Completed, ct); } //Other job handlers here... ///////////////////////////////////////////////////////////////////// }//eoc }//eons