using System; using System.Linq; using System.Globalization; using System.Text; using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.EntityFrameworkCore; using AyaNova.Util; using AyaNova.Models; //using System.Diagnostics; namespace AyaNova.Biz { internal static class NotifyEventHelper { private static ILogger log = AyaNova.Util.ApplicationLogging.CreateLogger("NotifyEventProcessor"); //Add operations message internal static async Task AddOpsProblemEvent(string message, Exception ex = null) { if (string.IsNullOrWhiteSpace(message) && ex == null) return; //Log as a backup in case there is no one to notify and also for the record and support if (ex != null) { //actually, if there is an exception it's already logged anyway so don't re-log it here, just makes dupes // log.LogError(ex, $"Ops problem notification: \"{message}\""); message += $"\nException error: {ExceptionUtil.ExtractAllExceptionMessages(ex)}"; } else log.LogWarning($"Ops problem notification: \"{message}\""); await AddGeneralNotifyEvent(NotifyEventType.ServerOperationsProblem, message, "OPS"); } //This handles general notification events not requiring a decision or tied to an object that are basically just a immediate message to the user //e.g. ops problems, GeneralNotification, NotifyHealthCheck etc //optional user id to send directly to them internal static async Task AddGeneralNotifyEvent(NotifyEventType eventType, string message, string name, Exception except = null, long userId = 0) { log.LogDebug($"AddGeneralNotifyEvent processing: [type:{eventType}, userId:{userId}, message:{message}]"); #if (DEBUG) switch (eventType) { case NotifyEventType.BackupStatus: case NotifyEventType.GeneralNotification: case NotifyEventType.NotifyHealthCheck://created by job processor itself case NotifyEventType.ServerOperationsProblem: break; default://this will likely be a development error, not a production error so no need to log etc throw (new System.NotSupportedException($"NotifyEventProcessor:AddGeneralNotifyEvent - Type of event {eventType} is unexpected and not supported")); } if (eventType != NotifyEventType.GeneralNotification && userId != 0) { throw (new System.NotSupportedException($"NotifyEventProcessor:AddGeneralNotifyEvent - event {eventType} was specified with user id {userId} which is unexpected and not supported")); } #endif try { using (AyContext ct = AyaNova.Util.ServiceProviderProvider.DBContext) { //General notification goes to a specific user only //no need to consult subscriptions if (eventType == NotifyEventType.GeneralNotification) { if (userId == 0) { //this will likely be a development error, not a production error so no need to log etc throw new System.ArgumentException("NotifyEventProcessor:AddGeneralNotifyEvent: GeneralNotification requires a user id but none was specified"); } var UserName = await ct.User.AsNoTracking().Where(z => z.Id == userId).Select(z => z.Name).FirstOrDefaultAsync(); //if they don't have a regular inapp subscription create one now NotifySubscription defaultsub = await EnsureDefaultInAppUserNotificationSubscriptionExists(userId, ct); if (string.IsNullOrWhiteSpace(name)) { name = UserName; } NotifyEvent n = new NotifyEvent() { EventType = eventType, UserId = userId, Message = message, NotifySubscriptionId = defaultsub.Id, Name = name }; await ct.NotifyEvent.AddAsync(n); await ct.SaveChangesAsync(); return; } //check subscriptions for event and send accordingly to each user var subs = await ct.NotifySubscription.Where(z => z.EventType == eventType).ToListAsync(); //append exception message if not null if (except != null) message += $"\nException error: {ExceptionUtil.ExtractAllExceptionMessages(except)}"; foreach (var sub in subs) { //note flag ~SERVER~ means to client to substitute "Server" translation key text instead NotifyEvent n = new NotifyEvent() { EventType = eventType, UserId = sub.UserId, Message = message, NotifySubscriptionId = sub.Id, Name = "~SERVER~" }; await ct.NotifyEvent.AddAsync(n); } if (subs.Count > 0) await ct.SaveChangesAsync(); } } catch (Exception ex) { log.LogError(ex, $"Error adding general notify event [type:{eventType}, userId:{userId}, message:{message}]"); } }//eom //This is told about an event and then determines if there are any subscriptions related to that event and proceses them accordingly //todo: this should take some kind of general event type like the AyaEvent types (i.e. which CRUD operation is in effect if relevant) //and also a biz object before and after or just before if not a change and also a AyaType //then *this* code will go through and look for subscriptions related to that event //this way the biz object code can be "dumb" about notifications in general and just let this code handle it as needed //will iterate the subscriptions and see if any apply here internal static async Task HandlePotentialNotificationEvent(AyaEvent ayaEvent, ICoreBizObjectModel newObject, ICoreBizObjectModel originalObject = null) { if (ServerBootConfig.SEEDING) return; log.LogDebug($"HandlePotentialNotificationEvent processing: [AyaType:{newObject.AyaType}, AyaEvent:{ayaEvent}]"); //set to true if any changes are made to the context (NotifyEvent added) bool SaveContext = false; try { using (AyContext ct = AyaNova.Util.ServiceProviderProvider.DBContext) { //short circuit if there are no subscriptions which would be typical of a v8 migrate scenario or fresh import or just not using notification if (!await ct.NotifySubscription.AnyAsync()) return; //switch through AyaEvent then individual Ayatypes as required for event types switch (ayaEvent) { case AyaEvent.Created: #region Created processing //------------------------------ // AyaType Specific created related subscriptions // Note: these are for specific things only in this block // generally being created notifications are further down below switch (newObject.AyaType) { case AyaType.Review: { //set a deadman automatic internal notification if goes past due var r = (Review)newObject; //if not completed yet and not overdue already (which could indicate an import or something) if (r.CompletedDate == null && r.DueDate > DateTime.UtcNow) { var userNotifySub = await EnsureDefaultInAppUserNotificationSubscriptionExists(r.UserId, ct); NotifySubscription supervisorNotifySub = null; if (r.UserId != r.AssignedByUserId) { supervisorNotifySub = await EnsureDefaultInAppUserNotificationSubscriptionExists(r.AssignedByUserId, ct); } { NotifyEvent n = new NotifyEvent() { EventType = NotifyEventType.GeneralNotification, UserId = r.UserId, ObjectId = newObject.Id, AyaType = AyaType.Review, NotifySubscriptionId = userNotifySub.Id, Name = "LT:ReviewOverDue - " + newObject.Name, EventDate = r.DueDate }; await ct.NotifyEvent.AddAsync(n); log.LogDebug($"Adding NotifyEvent: [{n.ToString()}]"); } if (supervisorNotifySub != null) { NotifyEvent n = new NotifyEvent() { EventType = NotifyEventType.GeneralNotification, UserId = r.AssignedByUserId, ObjectId = newObject.Id, AyaType = AyaType.Review, NotifySubscriptionId = supervisorNotifySub.Id, Name = "LT:ReviewOverDue - " + newObject.Name, EventDate = r.DueDate }; await ct.NotifyEvent.AddAsync(n); log.LogDebug($"Adding NotifyEvent: [{n.ToString()}]"); } } } break; //AyaTypes with their own special notification related events case AyaType.WorkOrder: { //WorkorderStatusChange { throw new System.NotImplementedException("Awaiting workorder object completion"); var subs = await ct.NotifySubscription.Where(z => z.EventType == NotifyEventType.WorkorderStatusChange).ToListAsync(); foreach (var sub in subs) { if (TagsMatch(newObject.Tags, sub.Tags)) { NotifyEvent n = new NotifyEvent() { EventType = NotifyEventType.ObjectAge, UserId = sub.UserId, ObjectId = newObject.Id, NotifySubscriptionId = sub.Id, //TODO: IdValue=((WorkOrder)newObject).WorkorderStatusId Name = newObject.Name }; await ct.NotifyEvent.AddAsync(n); log.LogDebug($"Adding NotifyEvent: [{n.ToString()}]"); //Note: in same event but for MODIFY below if need to delete old notifyevent here // var deleteEventList = await ct.NotifyEvent.Where(z => z.ObjectId == ev.ObjectId && z.AyaType == ev.AyaType).ToListAsync(); // ct.NotifyEvent.RemoveRange(deleteEventList); SaveContext = true; } } } //ScheduledOnWorkorder https://rockfish.ayanova.com/default.htm#!/rfcaseEdit/892 //ScheduledOnWorkorderImminent https://rockfish.ayanova.com/default.htm#!/rfcaseEdit/892 //WorkorderFinishStatusOverdue //WorkorderFinished [USER, CUSTOMER], if customer then also CopyOfCustomerNotification https://rockfish.ayanova.com/default.htm#!/rfcaseEdit/3398 //OutsideServiceOverdue //OutsideServiceReceived https://rockfish.ayanova.com/default.htm#!/rfcaseEdit/892 //PartRequestReceived https://rockfish.ayanova.com/default.htm#!/rfcaseEdit/892 //CustomerServiceImminent ALSO CopyOfCustomerNotification //WorkorderCreatedForCustomer (conditional ID sb customer id) ALSO CopyOfCustomerNotification https://rockfish.ayanova.com/default.htm#!/rfcaseEdit/3398 //PartRequested https://rockfish.ayanova.com/default.htm#!/rfcaseEdit/892 //WorkorderTotalExceedsThreshold //WorkorderStatusAge } break; case AyaType.Quote: //QuoteStatusChange [USER, CUSTOMER] / ALSO CopyOfCustomerNotification https://rockfish.ayanova.com/default.htm#!/rfcaseEdit/3398 //QuoteStatusAge break; case AyaType.Contract: //ContractExpiring [CUSTOMER/USER] / ALSO CopyOfCustomerNotification https://rockfish.ayanova.com/default.htm#!/rfcaseEdit/3398 break; case AyaType.Reminder: //ReminderImminent break; case AyaType.Unit: //UnitWarrantyExpiry break; case AyaType.UnitMeterReading: //UnitMeterReadingMultipleExceeded break; } //----------------------------------------------- //ObjectAge // // { // var subs = await ct.NotifySubscription.Where(z => z.EventType == NotifyEventType.ObjectAge && z.AyaType == newObject.AyaType).ToListAsync(); // foreach (var sub in subs) // { // if (TagsMatch(newObject.Tags, sub.Tags)) // { // //Note: age is set by advance notice which is consulted by CoreJobNotify in it's run so the deliver date is not required here only the reference EventDate to check for deliver // //ObjectAge is determined by subscription AgeValue in combo with the EventDate NotifyEvent parameter which together determines at what age from notifyevent.EventDate it's considered for the event to have officially occured // //However delivery is determined by sub.advancenotice so all three values play a part // // // NotifyEvent n = new NotifyEvent() // { // EventType = NotifyEventType.ObjectAge, // UserId = sub.UserId, // AyaType = newObject.AyaType, // ObjectId = newObject.Id, // NotifySubscriptionId = sub.Id, // Name = newObject.Name // }; // await ct.NotifyEvent.AddAsync(n); // log.LogDebug($"Adding NotifyEvent: [{n.ToString()}]"); // SaveContext = true; // } // } // } //------------------------------------------ //ObjectCreated // // [USER for any type, CUSTOMER for workorder only] / if customer then CopyOfCustomerNotification https://rockfish.ayanova.com/default.htm#!/rfcaseEdit/3398 // { // await ProcessStandardObjectCreatedEvents(newObject, ct); // } #endregion created processing break; case AyaEvent.Modified: #region Modified processing //######## NOTE: be sure to remove any that are being replaced potentially //------------------------------ // AyaType Specific modified related subscriptions // switch (newObject.AyaType) { case AyaType.Review: { // //Remove prior // await ClearPriorEventsForObject(ct, newObject.AyaType, newObject.Id, NotifyEventType.GeneralNotification);//assumes only general event for this object type is overdue here // //set a deadman automatic internal notification if goes past due // var r = (Review)newObject; // //it not completed yet and not overdue already (which could indicate an import or something) // if (r.CompletedDate == null && r.DueDate > DateTime.UtcNow) // { // var userNotifySub = await EnsureDefaultInAppUserNotificationSubscriptionExists(r.UserId, ct); // NotifySubscription supervisorNotifySub = null; // if (r.UserId != r.AssignedByUserId) // { // supervisorNotifySub = await EnsureDefaultInAppUserNotificationSubscriptionExists(r.AssignedByUserId, ct); // } // { // NotifyEvent n = new NotifyEvent() // { // EventType = NotifyEventType.GeneralNotification, // UserId = r.UserId, // ObjectId = newObject.Id, // AyaType = AyaType.Review, // NotifySubscriptionId = userNotifySub.Id, // Name = "LT:ReviewOverDue - " + newObject.Name, // EventDate = r.DueDate // }; // await ct.NotifyEvent.AddAsync(n); // log.LogDebug($"Adding NotifyEvent: [{n.ToString()}]"); // } // if (supervisorNotifySub != null) // { // NotifyEvent n = new NotifyEvent() // { // EventType = NotifyEventType.GeneralNotification, // UserId = r.AssignedByUserId, // ObjectId = newObject.Id, // AyaType = AyaType.Review, // NotifySubscriptionId = supervisorNotifySub.Id, // Name = "LT:ReviewOverDue - " + newObject.Name, // EventDate = r.DueDate // }; // await ct.NotifyEvent.AddAsync(n); // log.LogDebug($"Adding NotifyEvent: [{n.ToString()}]"); // } // } } break; //AyaTypes with their own special notification related events case AyaType.WorkOrder: { //WorkorderStatusChange [USER, CUSTOMER BOTH] / CopyOfCustomerNotification https://rockfish.ayanova.com/default.htm#!/rfcaseEdit/3398 // { throw new System.NotImplementedException("Awaiting workorder object completion"); // if (((WorkOrder)newObject).WorkorderStatusId != ((WorkOrder)dbObject).WorkorderStatusId) // { // var subs = await ct.NotifySubscription.Where(z => z.EventType == NotifyEventType.WorkorderStatusChange).ToListAsync(); // foreach (var sub in subs) // { // if (TagsMatch(newObject.Tags, sub.Tags)) // { // NotifyEvent n = new NotifyEvent() // { // EventType = NotifyEventType.ObjectAge, // UserId = sub.UserId, // ObjectId = newObject.Id, // NotifySubscriptionId = sub.Id, // //TODO: IdValue=((WorkOrder)newObject).WorkorderStatusId // EventDate = DateTime.UtcNow, //Name=newObject.Name //, // Name=newObject.Name // }; // await ct.NotifyEvent.AddAsync(n); // log.LogDebug($"Adding NotifyEvent: [{n.ToString()}]"); // //Note: in same event but for MODIFY below if need to delete old notifyevent here // // var deleteEventList = await ct.NotifyEvent.Where(z => z.ObjectId == ev.ObjectId && z.AyaType == ev.AyaType).ToListAsync(); // // ct.NotifyEvent.RemoveRange(deleteEventList); // SaveContext = true; // } // } // } } //ScheduledOnWorkorder (DELETE OLD, USER / DATE COULD CHANGE) //https://rockfish.ayanova.com/default.htm#!/rfcaseEdit/892 //ScheduledOnWorkorderImminent (DELTE OLD) //https://rockfish.ayanova.com/default.htm#!/rfcaseEdit/892 //WorkorderFinishStatusOverdue (DELETE OLD) //WorkorderFinished [USER, CUSTOMER] ALSO CopyOfCustomerNotification applies here https://rockfish.ayanova.com/default.htm#!/rfcaseEdit/3398 //OutsideServiceOverdue (ALSO DELETE OLD) //OutsideServiceReceived //PartRequestReceived //CustomerServiceImminent (ALSO DELETE OLD) / ALSO CopyOfCustomerNotification https://rockfish.ayanova.com/default.htm#!/rfcaseEdit/3398 //PartRequested //https://rockfish.ayanova.com/default.htm#!/rfcaseEdit/892 //WorkorderTotalExceedsThreshold //WorkorderStatusAge (ALSO DELETE OLD) //https://rockfish.ayanova.com/default.htm#!/rfcaseEdit/1137 //WorkorderFinishedFollowUp (uses AGE after finished status) [USER] } break; case AyaType.Quote: //QuoteStatusChange [USER, CUSTOMER] ALSO CopyOfCustomerNotification https://rockfish.ayanova.com/default.htm#!/rfcaseEdit/3398 //QuoteStatusAge (DELETE OLD) break; case AyaType.Contract: //ContractExpiring (DELETE OLD) break; case AyaType.Reminder: //ReminderImminent (DELETE OLD) break; case AyaType.Unit: //UnitWarrantyExpiry (DELETE OLD) break; case AyaType.UnitMeterReading: //UnitMeterReadingMultipleExceeded //case 1254 // This is as a threshold multiple value, i.e. if it's passing a multiple of 50,000 then notify, or notify every 10,000 or something. // In saving code it just checks to see if it has crossed the multiple threshold, i.e. calculate the old multiple, calculate the new multiple if over then send notification immediate. break; case AyaType.CustomerServiceRequest: //CSRAccepted (DELETE OLD might have changed from one to the other) [CUSTOMER] ALSO CopyOfCustomerNotification https://rockfish.ayanova.com/default.htm#!/rfcaseEdit/3398 //CSRRejected (DELETE OLD might have changed from one to the other) [CUSTOMER] ALSO CopyOfCustomerNotification https://rockfish.ayanova.com/default.htm#!/rfcaseEdit/3398 break; case AyaType.ServiceBank: //ServiceBankDepleted https://rockfish.ayanova.com/default.htm#!/rfcaseEdit/472 break; case AyaType.User: //This one's a little different, if user has had roles changed, then pre-existing subs may not be allowed anymore //Remove any notification subscriptions user doesn't have rights to: if (((User)originalObject).Roles != ((User)newObject).Roles) { var DeleteList = new List(); var NewRoles = ((User)newObject).Roles; //iterate subs and remove any user shouldn't have var userSubs = await ct.NotifySubscription.Where(z => z.UserId == newObject.Id).ToListAsync(); foreach (var sub in userSubs) { if (sub.AyaType != AyaType.NoType) { //check if user has rights to it or not still //must have read rights to be valid if (!AyaNova.Api.ControllerHelpers.Authorized.HasAnyRole(NewRoles, sub.AyaType)) { //no rights whatsoever, so delete it DeleteList.Add(sub.Id); } } } if (DeleteList.Count > 0) { var NSB = NotifySubscriptionBiz.GetBiz(ct); foreach (var l in DeleteList) { await NSB.DeleteAsync(l); } } } break; } //------------------------------------------ //ObjectModified // { var subs = await ct.NotifySubscription.Where(z => z.EventType == NotifyEventType.ObjectModified && z.AyaType == newObject.AyaType).ToListAsync(); foreach (var sub in subs) { if (TagsMatch(newObject.Tags, sub.Tags)) { NotifyEvent n = new NotifyEvent() { EventType = NotifyEventType.ObjectModified, UserId = sub.UserId, AyaType = newObject.AyaType, ObjectId = newObject.Id, NotifySubscriptionId = sub.Id, Name = newObject.Name }; await ct.NotifyEvent.AddAsync(n); log.LogDebug($"Adding NotifyEvent: [{n.ToString()}]"); SaveContext = true; } } } #endregion modified processing break; case AyaEvent.Deleted: #region Deleted processing //------------------------------ // Delete any NotifyEvent records for this exact object await ProcessStandardObjectDeletedEvents(newObject, ct); #endregion deleted processing break; default: #if (DEBUG) throw (new System.NotSupportedException($"NotifyEventProcessor:HandlePotentialNotificationEvent - AyaEvent {ayaEvent} was specified which is unexpected and not supported")); #else break; #endif } if (SaveContext) await ct.SaveChangesAsync(); } } catch (Exception ex) { log.LogError(ex, $"Error processing event"); } finally { log.LogDebug($"Notify event processing completed"); } }//eom /////////////////////////////////////////// // ENSURE USER HAS IN APP NOTIFICATION // // public static async Task EnsureDefaultInAppUserNotificationSubscriptionExists(long userId, AyContext ct) { var defaultsub = await ct.NotifySubscription.FirstOrDefaultAsync(z => z.EventType == NotifyEventType.GeneralNotification && z.UserId == userId && z.DeliveryMethod == NotifyDeliveryMethod.App); if (defaultsub == null) { //NOTE: agevalue and advanced notice settings here will ensure that direct in app notifications with a future delivery date ("deadman" switch deliveries) set in their //notifyevent.eventdate will deliver on that date and not immediately to support all the things that are direct built in notifications for future dates //such as for an overdue Review which doesn't have or need it's own notifyeventtype and subscription independently defaultsub = new NotifySubscription() { UserId = userId, EventType = NotifyEventType.GeneralNotification, DeliveryMethod = NotifyDeliveryMethod.App, AgeValue = new TimeSpan(0, 0, 1), AdvanceNotice = new TimeSpan(0, 0, 1) }; await ct.NotifySubscription.AddAsync(defaultsub); await ct.SaveChangesAsync(); } return defaultsub; } ///////////////////////////////////////// // PROCESS STANDARD EVENTS // // public static async Task ProcessStandardObjectEvents(AyaEvent ayaEvent, ICoreBizObjectModel newObject, AyContext ct) { switch(ayaEvent){ case AyaEvent.Created: await ProcessStandardObjectCreatedEvents(newObject,ct); break; case AyaEvent.Deleted: await ProcessStandardObjectDeletedEvents(newObject,ct); break; case AyaEvent.Modified: await ProcessStandardObjectModifiedEvents(newObject,ct); break; } } ///////////////////////////////////////// // PROCESS STANDARD CREATE NOTIFICATION // // public static async Task ProcessStandardObjectCreatedEvents(ICoreBizObjectModel newObject, AyContext ct) { //CREATED SUBSCRIPTIONS { var subs = await ct.NotifySubscription.Where(z => z.EventType == NotifyEventType.ObjectCreated && z.AyaType == newObject.AyaType).ToListAsync(); foreach (var sub in subs) { if (TagsMatch(newObject.Tags, sub.Tags)) { NotifyEvent n = new NotifyEvent() { EventType = NotifyEventType.ObjectCreated, UserId = sub.UserId, AyaType = newObject.AyaType, ObjectId = newObject.Id, NotifySubscriptionId = sub.Id, Name = newObject.Name }; await ct.NotifyEvent.AddAsync(n); log.LogDebug($"Adding NotifyEvent: [{n.ToString()}]"); await ct.SaveChangesAsync(); } } } //AGE SUBSCRIPTIONS { var subs = await ct.NotifySubscription.Where(z => z.EventType == NotifyEventType.ObjectAge && z.AyaType == newObject.AyaType).ToListAsync(); foreach (var sub in subs) { if (TagsMatch(newObject.Tags, sub.Tags)) { //Note: age is set by advance notice which is consulted by CoreJobNotify in it's run so the deliver date is not required here only the reference EventDate to check for deliver //ObjectAge is determined by subscription AgeValue in combo with the EventDate NotifyEvent parameter which together determines at what age from notifyevent.EventDate it's considered for the event to have officially occured //However delivery is determined by sub.advancenotice so all three values play a part // NotifyEvent n = new NotifyEvent() { EventType = NotifyEventType.ObjectAge, UserId = sub.UserId, AyaType = newObject.AyaType, ObjectId = newObject.Id, NotifySubscriptionId = sub.Id, Name = newObject.Name }; await ct.NotifyEvent.AddAsync(n); log.LogDebug($"Adding NotifyEvent: [{n.ToString()}]"); await ct.SaveChangesAsync(); } } } } ///////////////////////////////////////// // PROCESS STANDARD DELETE NOTIFICATION // // public static async Task ProcessStandardObjectDeletedEvents(ICoreBizObjectModel bizObject, AyContext ct) { // It's gone and shouldn't have any events left for it await ClearPriorEventsForObject(ct, bizObject.AyaType, bizObject.Id); //------------------------------------------ //ObjectDeleted notification // { var subs = await ct.NotifySubscription.Where(z => z.EventType == NotifyEventType.ObjectDeleted && z.AyaType == bizObject.AyaType).ToListAsync(); foreach (var sub in subs) { if (TagsMatch(bizObject.Tags, sub.Tags)) { //TODO: On deliver should point to history event log record or take from there and insert into delivery message? NotifyEvent n = new NotifyEvent() { EventType = NotifyEventType.ObjectDeleted, UserId = sub.UserId, AyaType = bizObject.AyaType, ObjectId = bizObject.Id, NotifySubscriptionId = sub.Id, Name = bizObject.Name }; await ct.NotifyEvent.AddAsync(n); log.LogDebug($"Adding NotifyEvent: [{n.ToString()}]"); await ct.SaveChangesAsync(); } } } } ////////////////////////////////// // CLEAN OUT OLD EVENTS // // public static async Task ClearPriorEventsForObject(AyContext ct, AyaType ayaType, long objectId, NotifyEventType eventType) { var eventsToDelete = await ct.NotifyEvent.Where(z => z.AyaType == ayaType && z.ObjectId == objectId && z.EventType == eventType).ToListAsync(); if (eventsToDelete.Count == 0) return; ct.NotifyEvent.RemoveRange(eventsToDelete); await ct.SaveChangesAsync(); } public static async Task ClearPriorEventsForObject(AyContext ct, AyaType ayaType, long objectId) { var eventsToDelete = await ct.NotifyEvent.Where(z => z.AyaType == ayaType && z.ObjectId == objectId).ToListAsync(); if (eventsToDelete.Count == 0) return; ct.NotifyEvent.RemoveRange(eventsToDelete); await ct.SaveChangesAsync(); } ////////////////////////////////// // COMPARE TAGS COLLECTION // // public static bool TagsMatch(List objectTags, List subTags) { //no subscription tags? Then it always will match if (subTags.Count == 0) return true; //have sub tags but object has none? Then it's never going to match if (objectTags.Count == 0) return false; //not enought tags on object to match sub tags? if (subTags.Count > objectTags.Count) return false; //ok, here it's worth checking it out return subTags.All(z => objectTags.Any(x => x == z)); } }//eoc }//eons