using Ganss.Xss; using Microsoft.EntityFrameworkCore; using RMuseum.DbContext; using RMuseum.Models.Ganjoor; using RSecurityBackend.Models.Generic; using RSecurityBackend.Models.Generic.Db; using RSecurityBackend.Services.Implementation; using System; using System.Collections.Generic; using System.Data; using System.Linq; using System.Net.Http; using System.Text.RegularExpressions; using System.Threading.Tasks; namespace RMuseum.Services.Implementation { /// /// IGanjoorService implementation /// public partial class GanjoorService : IGanjoorService { private async Task _FindCategoryPoemsRhythmsInternal(int catId, bool retag, string rhythm) { using (RMuseumDbContext context = new RMuseumDbContext(new DbContextOptions())) //this is long running job, so _context might be already been freed/collected by GC { LongRunningJobProgressServiceEF jobProgressServiceEF = new LongRunningJobProgressServiceEF(context); var job = (await jobProgressServiceEF.NewJob($"FindCategoryPoemsRhythms Cat {catId}", "Query data")).Result; await _FindCategoryPoemsRhythmsMoreInternal(context, jobProgressServiceEF, job, catId, retag, rhythm); await jobProgressServiceEF.UpdateJob(job.Id, 0, "SubCats"); List catListId = new List(); await _populateCategoryChildren(context, catId, catListId); foreach (var subCatId in catListId) { await jobProgressServiceEF.UpdateJob(job.Id, subCatId, "SubCats"); await _FindCategoryPoemsRhythmsMoreInternal(context, jobProgressServiceEF, job, subCatId, retag, rhythm); } await jobProgressServiceEF.UpdateJob(job.Id, 100, "", true); } } private async Task _FindCategoryPoemsRhythmsMoreInternal(RMuseumDbContext context, LongRunningJobProgressServiceEF jobProgressServiceEF, RLongRunningJobStatus job, int catId, bool retag, string rhythm) { try { var metres = await context.GanjoorMetres.OrderBy(m => m.Rhythm).AsNoTracking().ToArrayAsync(); var rhythms = metres.Select(m => m.Rhythm).ToArray(); GanjoorMetre preDeterminedMetre = string.IsNullOrEmpty(rhythm) ? null : metres.Where(m => m.Rhythm == rhythm).Single(); var poems = await context.GanjoorPoems.AsNoTracking().Where(p => p.CatId == catId).ToListAsync(); int i = 0; List> updateList = new List>(); using (HttpClient httpClient = new HttpClient()) { foreach (var poem in poems) { var sections = await context.GanjoorPoemSections.Where(p => p.PoemId == poem.Id).ToListAsync(); foreach (var section in sections.Where(s => s.GanjoorMetreRefSectionIndex == null).ToList()) { if (retag || section.GanjoorMetreId == null) { if (preDeterminedMetre == null) { var res = await _FindSectionRhythm(section, context, httpClient, rhythms); if (!string.IsNullOrEmpty(res.Result)) { section.GanjoorMetreId = metres.Where(m => m.Rhythm == res.Result).Single().Id; context.GanjoorPoemSections.Update(section); } } else { section.GanjoorMetreId = preDeterminedMetre.Id; context.GanjoorPoemSections.Update(section); } if (section.GanjoorMetreId != null) { var dependentSections = sections.Where(s => s.GanjoorMetreRefSectionIndex == section.Index).ToList(); foreach (var dsection in dependentSections) { dsection.GanjoorMetreId = section.GanjoorMetreId; context.GanjoorPoemSections.Update(dsection); } } if (section.GanjoorMetreId != null && !string.IsNullOrEmpty(section.RhymeLetters)) { if (!updateList.Any(u => u.Item1 == section.GanjoorMetreId && u.Item2 == section.RhymeLetters)) { updateList.Add(new Tuple((int)section.GanjoorMetreId, section.RhymeLetters)); } } if (section.GanjoorMetreId != null) { var dependentSections = sections.Where(s => s.GanjoorMetreRefSectionIndex == section.Index).ToList(); foreach (var dsection in dependentSections) { if (dsection.GanjoorMetreId != null && !string.IsNullOrEmpty(dsection.RhymeLetters)) { if (!updateList.Any(u => u.Item1 == dsection.GanjoorMetreId && u.Item2 == dsection.RhymeLetters)) { updateList.Add(new Tuple((int)dsection.GanjoorMetreId, dsection.RhymeLetters)); } } } } await jobProgressServiceEF.UpdateJob(job.Id, i++); } } } } foreach (var item in updateList) { await _UpdateRelatedSections(context, item.Item1, item.Item2, jobProgressServiceEF, job); } var subCats = await context.GanjoorCategories.AsNoTracking().Where(c => c.ParentId == catId).ToListAsync(); foreach (var subCat in subCats) { await jobProgressServiceEF.UpdateJob(job.Id, 0, $"FindCategoryPoemsRhythms Cat {subCat.Id}"); await _FindCategoryPoemsRhythmsMoreInternal(context, jobProgressServiceEF, job, subCat.Id, retag, rhythm); } } catch (Exception exp) { await jobProgressServiceEF.UpdateJob(job.Id, 100, "", false, exp.ToString()); } } /// /// find category poem rhymes /// /// /// /// /// public RServiceResult FindCategoryPoemsRhythms(int catId, bool retag, string rhythm = "") { _backgroundTaskQueue.QueueBackgroundWorkItem ( async token => { await _FindCategoryPoemsRhythmsInternal(catId, retag, rhythm); }); return new RServiceResult(true); } /// /// directly insert generated TOC /// /// /// /// /// public async Task> DirectInsertGeneratedTableOfContents(int catId, Guid userId, GanjoorTOC options) { return await _DirectInsertGeneratedTableOfContents(_context, catId, userId, options); } /// /// generate category TOC /// /// /// /// /// public async Task> GenerateTableOfContents(Guid userId, int catId, GanjoorTOC options) { return await _GenerateTableOfContents(_context, catId, options, userId); } /// /// make plain text /// /// /// private static string PreparePlainText(List verses) { string plainText = ""; foreach (GanjoorVerse verse in verses) { plainText += $"{LanguageUtils.MakeTextSearchable(verse.Text)}{Environment.NewLine}";//replace zwnj with space } return plainText.Trim(); } /// /// separate verses in poem.PlainText with Environment.NewLine instead of SPACE /// /// /// public RServiceResult RegerneratePoemsPlainText(int catId) { _backgroundTaskQueue.QueueBackgroundWorkItem ( async token => { using (RMuseumDbContext context = new RMuseumDbContext(new DbContextOptions())) //this is long running job, so _context might be already been freed/collected by GC { LongRunningJobProgressServiceEF jobProgressServiceEF = new LongRunningJobProgressServiceEF(context); var job = (await jobProgressServiceEF.NewJob($"RegerneratePoemsPlainText {catId}", "Query data")).Result; try { var poems = catId == 0 ? await context.GanjoorPoems.ToArrayAsync() : await context.GanjoorPoems.Where(p => p.CatId == catId).ToArrayAsync(); await jobProgressServiceEF.UpdateJob(job.Id, 0, $"Updating PlainText/Poem Html {catId}"); int percent = 0; for (int i = 0; i < poems.Length; i++) { if (i * 100 / poems.Length > percent) { percent++; await jobProgressServiceEF.UpdateJob(job.Id, percent); } var poem = poems[i]; var verses = await context.GanjoorVerses.AsNoTracking().Where(v => v.PoemId == poem.Id).OrderBy(v => v.VOrder).ToListAsync(); poem.PlainText = PreparePlainText(verses); poem.HtmlText = PrepareHtmlText(verses); } await jobProgressServiceEF.UpdateJob(job.Id, 0, $"Finalizing PlainText/Poem Html {catId}"); context.GanjoorPoems.UpdateRange(poems); await context.SaveChangesAsync(); await jobProgressServiceEF.UpdateJob(job.Id, 50, $"Updating pages HTML {catId}"); //the following line always gets timeout, so it is being replaced by a loop //await context.Database.ExecuteSqlRawAsync( // "UPDATE p SET p.HtmlText = (SELECT poem.HtmlText FROM GanjoorPoems poem WHERE poem.Id = p.Id) FROM GanjoorPages p WHERE p.GanjoorPageType = 3 "); foreach (var poem in poems) { var page = await context.GanjoorPages.Where(p => p.Id == poem.Id).SingleAsync(); page.HtmlText = poem.HtmlText; context.GanjoorPages.Update(page); } await context.SaveChangesAsync(); await jobProgressServiceEF.UpdateJob(job.Id, 100, "", true); } catch (Exception exp) { await jobProgressServiceEF.UpdateJob(job.Id, 100, "", false, exp.ToString()); } } } ); return new RServiceResult(true); } /// /// examine site pages for broken links /// /// public RServiceResult HealthCheckContents() { _backgroundTaskQueue.QueueBackgroundWorkItem ( async token => { using (RMuseumDbContext context = new RMuseumDbContext(new DbContextOptions())) //this is long running job, so _context might be already been freed/collected by GC { LongRunningJobProgressServiceEF jobProgressServiceEF = new LongRunningJobProgressServiceEF(context); var job = (await jobProgressServiceEF.NewJob("HealthCheckContents", "Query data")).Result; try { var pages = await context.GanjoorPages.ToArrayAsync(); await jobProgressServiceEF.UpdateJob(job.Id, 0, $"Examining Pages"); var previousErrors = await context.GanjoorHealthCheckErrors.ToArrayAsync(); context.RemoveRange(previousErrors); await context.SaveChangesAsync(); int percent = 0; for (int i = 0; i < pages.Length; i++) { if (i * 100 / pages.Length > percent) { percent++; await jobProgressServiceEF.UpdateJob(job.Id, percent); } var hrefs = pages[i].HtmlText.Split(new[] { "href=\"" }, StringSplitOptions.RemoveEmptyEntries).Where(o => o.StartsWith("http")).Select(o => o.Substring(0, o.IndexOf("\""))); foreach (string url in hrefs) { if (url == "https://ganjoor.net" || url == "https://ganjoor.net/" || url.IndexOf("https://ganjoor.net/vazn/?") == 0 || url.IndexOf("https://ganjoor.net/simi/?v") == 0) continue; if (url.IndexOf("http://ganjoor.net") == 0) { context.GanjoorHealthCheckErrors.Add ( new GanjoorHealthCheckError() { ReferrerPageUrl = pages[i].FullUrl, TargetUrl = url, BrokenLink = false, MulipleTargets = false } ); await context.SaveChangesAsync(); } else if (url.IndexOf("https://ganjoor.net") == 0) { var testUrl = url.Substring("https://ganjoor.net".Length); if (testUrl[testUrl.Length - 1] == '/') testUrl = testUrl.Substring(0, testUrl.Length - 1); var pageCount = await context.GanjoorPages.Where(p => p.FullUrl == testUrl).CountAsync(); if (pageCount != 1) { context.GanjoorHealthCheckErrors.Add ( new GanjoorHealthCheckError() { ReferrerPageUrl = pages[i].FullUrl, TargetUrl = url, BrokenLink = pageCount == 0, MulipleTargets = pageCount != 0 } ); await context.SaveChangesAsync(); } } } } await jobProgressServiceEF.UpdateJob(job.Id, 100, "", true); } catch (Exception exp) { await jobProgressServiceEF.UpdateJob(job.Id, 100, "", false, exp.ToString()); } } } ); return new RServiceResult(true); } private async Task _ProcessCommentHtml(string commentText, RMuseumDbContext context) { // Use a proper HTML sanitizer var sanitizer = new HtmlSanitizer(); // Configure allowed tags sanitizer.AllowedTags.Clear(); sanitizer.AllowedTags.Add("p"); sanitizer.AllowedTags.Add("a"); sanitizer.AllowedTags.Add("br"); sanitizer.AllowedTags.Add("b"); sanitizer.AllowedTags.Add("i"); sanitizer.AllowedTags.Add("strong"); sanitizer.AllowedTags.Add("img"); sanitizer.AllowedTags.Add("span"); // Configure allowed attributes sanitizer.AllowedAttributes.Clear(); sanitizer.AllowedAttributes.Add("href"); sanitizer.AllowedAttributes.Add("src"); sanitizer.AllowedAttributes.Add("alt"); sanitizer.AllowedAttributes.Add("title"); sanitizer.AllowedAttributes.Add("rel"); // IMPORTANT: Remove all style attributes to prevent colored text and font changes sanitizer.AllowedAttributes.Remove("style"); // Disallow any CSS or style-related attributes sanitizer.AllowedCssProperties.Clear(); // Additional security: disallow data URIs and javascript: protocols sanitizer.AllowedSchemes.Clear(); sanitizer.AllowedSchemes.Add("http"); sanitizer.AllowedSchemes.Add("https"); sanitizer.AllowedSchemes.Add("mailto"); sanitizer.AllowedSchemes.Add("ftp"); // Sanitize the HTML string sanitizedHtml = sanitizer.Sanitize(commentText); // Process URLs (Linkify) and internal Ganjoor links sanitizedHtml = await _ProcessUrls(sanitizedHtml, context); return sanitizedHtml; } private async Task _ProcessUrls(string html, RMuseumDbContext context) { // First, process any existing href links html = await _ProcessExistingLinks(html, context); // Then linkify any bare URLs that aren't already in links if (!html.Contains("href=\"") && html.Contains("http")) { html = _Linkify(html); } return html; } private async Task _ProcessExistingLinks(string html, RMuseumDbContext context) { // Use regex to find and process all href attributes var hrefRegex = new Regex(@"]*?\s+)?href=""([^""]*)""[^>]*>(.*?)", RegexOptions.IgnoreCase | RegexOptions.Singleline); var matches = hrefRegex.Matches(html); var result = html; var replacements = new List<(string old, string @new)>(); foreach (Match match in matches) { string url = match.Groups[1].Value; string linkText = match.Groups[2].Value; string newLink = await _ProcessSingleLink(url, linkText, context); // Store the replacement replacements.Add((match.Value, newLink)); } // Apply replacements from end to start to maintain indices foreach (var replacement in replacements.OrderByDescending(r => result.IndexOf(r.old))) { result = result.Replace(replacement.old, replacement.@new); } return result; } private async Task _ProcessSingleLink(string url, string linkText, RMuseumDbContext context) { // If link text is the same as URL (auto-generated link), try to improve it if (url == linkText || linkText.Trim() == url.Trim()) { // Process Ganjoor internal links if (url.StartsWith("http://ganjoor.net") || url.StartsWith("https://ganjoor.net")) { string path = url.Replace("http://ganjoor.net", "").Replace("https://ganjoor.net", ""); int coupletNumber = -1; string cleanPath = path; // Extract couplet number if present if (path.Contains("#bn")) { int coupletStartIndex = path.IndexOf("#bn") + "#bn".Length; if (int.TryParse(path.Substring(coupletStartIndex), out coupletNumber)) { cleanPath = path.Substring(0, path.IndexOf("#bn")); } } // Remove trailing slash if (cleanPath.Length > 0 && cleanPath[cleanPath.Length - 1] == '/') cleanPath = cleanPath.Substring(0, cleanPath.Length - 1); var page = await context.GanjoorPages .AsNoTracking() .Where(p => p.FullUrl == cleanPath) .FirstOrDefaultAsync(); if (page != null) { string displayText = page.FullTitle; // Add couplet summary if applicable if (coupletNumber != -1) { string coupletSummary = await _GetCoupletSummary(page.Id, coupletNumber); if (!string.IsNullOrEmpty(coupletSummary)) { displayText = $"{page.FullTitle} » {coupletSummary}"; } } return $@"{displayText}"; } } else { // External link - use generic text return $@"پیوند به وبگاه بیرونی"; } } // If link text was manually provided, keep it but still validate the link return $@"{linkText}"; } private async Task _GetCoupletSummary(int poemId, int coupletNumber) { int coupletIndex = coupletNumber - 1; var verses = await _context.GanjoorVerses .Where(v => v.PoemId == poemId) .OrderBy(v => v.VOrder) .ToListAsync(); int cIndex = -1; for (int i = 0; i < verses.Count; i++) { if (verses[i].VersePosition != VersePosition.Left && verses[i].VersePosition != VersePosition.CenteredVerse2) cIndex++; if (cIndex == coupletIndex) { string coupletSummary = verses[i].Text; if (verses[i].VersePosition == VersePosition.Right && i < verses.Count - 1) { coupletSummary += $" {verses[i + 1].Text}"; } else if (verses[i].VersePosition == VersePosition.CenteredVerse1 && i < verses.Count - 1 && verses[i + 1].VersePosition == VersePosition.CenteredVerse2) { coupletSummary += $" {verses[i + 1].Text}"; } return _CutSummary(coupletSummary); } } return null; } private string _Linkify(string text) { // Improved Linkify with better URL detection var urlRegex = new Regex(@"(https?://[^\s<>""']+)", RegexOptions.IgnoreCase); return urlRegex.Replace(text, match => { string url = match.Value; return $@"{url}"; }); } /// /// examine comments for long links /// /// public RServiceResult FindAndFixLongUrlsInComments() { _backgroundTaskQueue.QueueBackgroundWorkItem ( async token => { using (RMuseumDbContext context = new RMuseumDbContext(new DbContextOptions())) //this is long running job, so _context might be already been freed/collected by GC { LongRunningJobProgressServiceEF jobProgressServiceEF = new LongRunningJobProgressServiceEF(context); var job = (await jobProgressServiceEF.NewJob("FindAndFixLongUrlsInComments", "Query data")).Result; try { var comments = //await context.GanjoorComments.Where(c => c.HtmlComment.Contains("href=")).ToArrayAsync(); await context.GanjoorComments.Where(c => c.HtmlComment.Contains("style")).ToArrayAsync(); await jobProgressServiceEF.UpdateJob(job.Id, 0, $"Examining {comments.Length} Comments"); int percent = 0; for (int i = 0; i < comments.Length; i++) { if (i * 100 / comments.Length > percent) { percent++; await jobProgressServiceEF.UpdateJob(job.Id, percent); } var comment = comments[i]; string commentText = await _ProcessCommentHtml(comment.HtmlComment, context); if (commentText != comment.HtmlComment) { comment.HtmlComment = commentText; context.Update(comment); await context.SaveChangesAsync(); } } await jobProgressServiceEF.UpdateJob(job.Id, 100, "", true); } catch (Exception exp) { await jobProgressServiceEF.UpdateJob(job.Id, 100, "", false, exp.ToString()); } } } ); return new RServiceResult(true); } /// /// start filling poems couplet indices /// /// public RServiceResult StartFillingPoemsCoupletIndices() { _backgroundTaskQueue.QueueBackgroundWorkItem ( async token => { using (RMuseumDbContext context = new RMuseumDbContext(new DbContextOptions())) //this is long running job, so _context might be already been freed/collected by GC { LongRunningJobProgressServiceEF jobProgressServiceEF = new LongRunningJobProgressServiceEF(context); var job = (await jobProgressServiceEF.NewJob("FillingPoemsCoupletIndices", "Query data")).Result; try { var poemIds = await context.GanjoorPoems.AsNoTracking().Select(p => p.Id).ToListAsync(); int percent = 0; for (int i = 0; i < poemIds.Count; i++) { if (i * 100 / poemIds.Count > percent) { percent++; await jobProgressServiceEF.UpdateJob(job.Id, percent); } await _FillPoemCoupletIndices(context, poemIds[i]); } await jobProgressServiceEF.UpdateJob(job.Id, 100, "", true); } catch (Exception exp) { await jobProgressServiceEF.UpdateJob(job.Id, 100, "", false, exp.ToString()); } } } ); return new RServiceResult(true); } /// /// refill couplet indices /// /// /// public async Task> RefillCoupletIndicesAsync(int poemId) { try { await _FillPoemCoupletIndices(_context, poemId); await _context.SaveChangesAsync(); return new RServiceResult(false); } catch (Exception exp) { return new RServiceResult(false, exp.ToString()); } } /// /// fill section couplets count /// /// public RServiceResult StartFillingSectionCoupletCounts() { _backgroundTaskQueue.QueueBackgroundWorkItem ( async token => { using (RMuseumDbContext context = new RMuseumDbContext(new DbContextOptions())) //this is long running job, so _context might be already been freed/collected by GC { LongRunningJobProgressServiceEF jobProgressServiceEF = new LongRunningJobProgressServiceEF(context); var job = (await jobProgressServiceEF.NewJob("FillingSectionCoupletCounts", "Query data")).Result; try { var sectionIds = await context.GanjoorPoemSections.AsNoTracking().Select(p => p.Id).ToListAsync(); int percent = 0; for (int i = 0; i < sectionIds.Count; i++) { if (i * 100 / sectionIds.Count > percent) { percent++; await jobProgressServiceEF.UpdateJob(job.Id, percent); } await _FillSectionCoupletCountsAsync(context, sectionIds[i]); } await jobProgressServiceEF.UpdateJob(job.Id, 100, "", true); } catch (Exception exp) { await jobProgressServiceEF.UpdateJob(job.Id, 100, "", false, exp.ToString()); } } } ); return new RServiceResult(true); } private async Task _FillSectionCoupletCountsAsync(RMuseumDbContext context, int sectionId) { var section = await context.GanjoorPoemSections.Where(s => s.Id == sectionId).SingleAsync(); section.CoupletsCount = await context.GanjoorVerses.Where( v => v.PoemId == section.PoemId && ( (section.VerseType == VersePoemSectionType.First && v.SectionIndex1 == section.Index) || (section.VerseType == VersePoemSectionType.Second && v.SectionIndex2 == section.Index) || (section.VerseType == VersePoemSectionType.Third && v.SectionIndex3 == section.Index) || (section.VerseType == VersePoemSectionType.Forth && v.SectionIndex4 == section.Index)) && (v.VersePosition == VersePosition.Left || v.VersePosition == VersePosition.CenteredVerse1) ).CountAsync(); context.Update(section); } /// /// regenerate poem full titles to fix an old bug /// /// public RServiceResult RegeneratePoemsFullTitles() { _backgroundTaskQueue.QueueBackgroundWorkItem ( async token => { using (RMuseumDbContext context = new RMuseumDbContext(new DbContextOptions())) //this is long running job, so _context might be already been freed/collected by GC { LongRunningJobProgressServiceEF jobProgressServiceEF = new LongRunningJobProgressServiceEF(context); var job = (await jobProgressServiceEF.NewJob("RegenratePoemsFullTitles", "Query data")).Result; int percent = 0; try { var poemIds = await context.GanjoorPoems.AsNoTracking().Select(p => p.Id).ToListAsync(); for (int i = 0; i < poemIds.Count; i++) { if (i * 100 / poemIds.Count > percent) { percent++; await jobProgressServiceEF.UpdateJob(job.Id, percent); } var poem = await context.GanjoorPoems.Where(p => p.Id == poemIds[i]).SingleOrDefaultAsync(); var catPage = await context.GanjoorPages.AsNoTracking().Where(p => p.GanjoorPageType == GanjoorPageType.CatPage && p.CatId == poem.CatId).SingleOrDefaultAsync(); if (catPage == null) { catPage = await context.GanjoorPages.AsNoTracking().Where(p => p.GanjoorPageType == GanjoorPageType.PoetPage && p.CatId == poem.CatId).SingleOrDefaultAsync(); } var title = $"{catPage.FullTitle} » {poem.Title}"; if (title != poem.FullTitle) { poem.FullTitle = title; context.Update(poem); } var page = await context.GanjoorPages.Where(p => p.Id == poem.Id).SingleOrDefaultAsync(); if (title != page.FullTitle) { page.FullTitle = title; context.Update(page); } } await jobProgressServiceEF.UpdateJob(job.Id, 100, "", true); } catch (Exception exp) { await jobProgressServiceEF.UpdateJob(job.Id, percent, "", false, exp.ToString()); } } } ); return new RServiceResult(true); } /// /// start finding rhymes for single couplets /// /// public RServiceResult StartFindingRhymesForSingleCouplets() { _backgroundTaskQueue.QueueBackgroundWorkItem ( async token => { using (RMuseumDbContext context = new RMuseumDbContext(new DbContextOptions())) //this is long running job, so _context might be already been freed/collected by GC { LongRunningJobProgressServiceEF jobProgressServiceEF = new LongRunningJobProgressServiceEF(context); var job = (await jobProgressServiceEF.NewJob("FindingRhymesForSingleCouplets", "Query data")).Result; try { var sections = await context.GanjoorPoemSections.AsNoTracking().Include(s => s.GanjoorMetre).Where(s => s.SectionType == PoemSectionType.WholePoem && s.GanjoorMetre != null && (s.RhymeLetters.Length >= 30 || s.RhymeLetters.Length < 2)).ToListAsync(); int percent = 0; for (int i = 0; i < sections.Count; i++) { if (i * 100 / sections.Count > percent) { percent++; await jobProgressServiceEF.UpdateJob(job.Id, percent); } var res = await _FindSectionRhyme(context, sections[i].Id); if (string.IsNullOrEmpty(res.ExceptionString)) { if (!string.IsNullOrEmpty(res.Result.Rhyme) && res.Result.Rhyme != sections[i].RhymeLetters) { var sectionTracked = await context.GanjoorPoemSections.Where(s => s.Id == sections[i].Id).SingleAsync(); var oldRhyme = sectionTracked.RhymeLetters; sectionTracked.RhymeLetters = res.Result.Rhyme; context.Update(sectionTracked); await context.SaveChangesAsync(); await _UpdateRelatedSections(context, (int)sectionTracked.GanjoorMetreId, oldRhyme, jobProgressServiceEF, job, percent); await _UpdateRelatedSections(context, (int)sectionTracked.GanjoorMetreId, res.Result.Rhyme, jobProgressServiceEF, job, percent); } } } await jobProgressServiceEF.UpdateJob(job.Id, 100, "", true); } catch (Exception exp) { await jobProgressServiceEF.UpdateJob(job.Id, 100, "", false, exp.ToString()); } } } ); return new RServiceResult(true); } } }