diff --git a/src/apps/Caspian.WorkflowFramework.Sample/Controllers/WorkflowController.cs b/src/apps/Caspian.WorkflowFramework.Sample/Controllers/WorkflowController.cs index 7e21184..b250896 100644 --- a/src/apps/Caspian.WorkflowFramework.Sample/Controllers/WorkflowController.cs +++ b/src/apps/Caspian.WorkflowFramework.Sample/Controllers/WorkflowController.cs @@ -97,7 +97,8 @@ public class WorkflowController : Controller, ILoggable { _logger.Log("Start of Cancel action", WFInstanceId: input.WorkflowInstanceId, JsonConvert.SerializeObject(input)); - var result = await _workflowService.CancelInstanceAsync(input); + //var result = await _workflowService.CancelInstanceAsync(input); + var result = await _workflowService.ForceCancelAsync(input); _logger.Log("End of Cancel action", WFInstanceId: input.WorkflowInstanceId, JsonConvert.SerializeObject(result)); @@ -160,4 +161,11 @@ public class WorkflowController : Controller, ILoggable return Json(result); } #endregion + + [HttpPost("GetNextPossibleEvents")] + public async Task>> GetNextPossibleEvents(string workflowInstanceId) + { + var result = await _workflowService.GetNextPossibleEventsAsync(workflowInstanceId); + return Ok(result); + } } diff --git a/src/apps/Caspian.WorkflowFramework/Contracts/IWorkflowService.cs b/src/apps/Caspian.WorkflowFramework/Contracts/IWorkflowService.cs index a8b63eb..f3f51be 100644 --- a/src/apps/Caspian.WorkflowFramework/Contracts/IWorkflowService.cs +++ b/src/apps/Caspian.WorkflowFramework/Contracts/IWorkflowService.cs @@ -50,6 +50,10 @@ public interface IWorkflowService /// Task> CancelInstanceAsync(CancelWorkflowInstanceModel input); + Task> ForceCancelAsync(CancelWorkflowInstanceModel input); + + Task> GetNextPossibleEventsAsync(string workflowInstanceId); + ///// ///// Evicts a workflow definition from cache (optional, commented out in service). ///// diff --git a/src/apps/Caspian.WorkflowFramework/Models/PossibleEventDto.cs b/src/apps/Caspian.WorkflowFramework/Models/PossibleEventDto.cs new file mode 100644 index 0000000..2baa15c --- /dev/null +++ b/src/apps/Caspian.WorkflowFramework/Models/PossibleEventDto.cs @@ -0,0 +1,13 @@ +namespace Caspian.WorkflowFramework.Models; +public sealed class PossibleEventDto +{ + public string EventName { get; set; } = default!; + public string? BookmarkId { get; set; } + public string? ActivityId { get; set; } + public string? ActivityNodeId { get; set; } + public string? ActivityInstanceId { get; set; } + public DateTimeOffset? CreatedAt { get; set; } + public bool? AutoBurn { get; set; } + public string? CallbackMethodName { get; set; } + public IDictionary? Metadata { get; set; } +} diff --git a/src/apps/Caspian.WorkflowFramework/Services/WorkflowService.cs b/src/apps/Caspian.WorkflowFramework/Services/WorkflowService.cs index 19d6d64..ff71582 100644 --- a/src/apps/Caspian.WorkflowFramework/Services/WorkflowService.cs +++ b/src/apps/Caspian.WorkflowFramework/Services/WorkflowService.cs @@ -11,7 +11,10 @@ using Elsa.Workflows.Management; using Elsa.Workflows.Management.Filters; using Elsa.Workflows.Models; using Elsa.Workflows.Runtime; +using Elsa.Workflows.Runtime.Filters; using Elsa.Workflows.Runtime.Messages; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.DependencyInjection; using Newtonsoft.Json; using Newtonsoft.Json.Converters; @@ -30,6 +33,9 @@ public class WorkflowService : IWorkflowService, ILoggable private readonly IEventPublisher _eventPublisher; private readonly IWorkflowDefinitionCacheManager _workflowDefinitionCacheManager; private readonly IMdbLogger _logger; + + private readonly IBookmarkStore _bookmarkStore; + private readonly IActivityExecutionStore _activityExecutionStore; #endregion #region Ctor @@ -44,6 +50,8 @@ public class WorkflowService : IWorkflowService, ILoggable _workflowInstanceStore = _serviceProvider.GetRequiredService(); _eventPublisher = _serviceProvider.GetRequiredService(); _logger = logger; + _bookmarkStore = _serviceProvider.GetRequiredService(); + _activityExecutionStore = _serviceProvider.GetRequiredService(); } #endregion @@ -725,6 +733,189 @@ public class WorkflowService : IWorkflowService, ILoggable } #endregion + #region ForceCancelAsync + public async Task> ForceCancelAsync(CancelWorkflowInstanceModel input) + { + var sw = Stopwatch.StartNew(); + try + { + _logger.Log("Start ForceCancelAsync", WFInstanceId: input.WorkflowInstanceId, input); + + var instance = await _workflowInstanceManager.FindByIdAsync(input.WorkflowInstanceId); + + if (instance == null) + return new WorkflowResponse { Status = 1, Message = "Instance not found", Data = false }; + + // جلوگری از Resume شدن دوباره + instance.Status = WorkflowStatus.Finished; + instance.SubStatus = WorkflowSubStatus.Cancelled; + + // پاک‌سازی bookmarks از دیتابیس واقعی + var bookmarkFilter = new BookmarkFilter() + { + WorkflowInstanceId = input.WorkflowInstanceId + }; + await _bookmarkStore.DeleteAsync(bookmarkFilter); + + // پاک‌سازی scheduled activities (در صورتی که runtime distributed باشد) + var activityFilter = new ActivityExecutionRecordFilter() + { + WorkflowInstanceId = input.WorkflowInstanceId + }; + await _activityExecutionStore.DeleteManyAsync(activityFilter); + + //// پاک‌سازی incidents (در پروژه‌های بزرگ ضروری است) + //await _incidentStore.DeleteManyAsync( + // x => x.WorkflowInstanceId == input.WorkflowInstanceId + //); + + // ذخیره instance تغییر یافته + await _workflowInstanceManager.SaveAsync(instance); + + sw.Stop(); + + var response = new WorkflowResponse + { + Status = 0, + Message = "Force cancel completed", + Data = true + }; + + _logger.Log("End ForceCancelAsync", WFInstanceId: instance.Id, response, sw.ElapsedMilliseconds); + + return response; + } + catch (Exception ex) + { + sw.Stop(); + + var response = new WorkflowResponse + { + Status = 100, + Message = ex.Message, + Data = false + }; + + _logger.Log("Exception in ForceCancelAsync", WFInstanceId: input.WorkflowInstanceId, response, sw.ElapsedMilliseconds, ex); + + return response; + } + } + + #endregion + + public async Task> GetNextPossibleEventsAsync(string workflowInstanceId) + { + var sw = Stopwatch.StartNew(); + try + { + _logger.Log("Start of GetNextPossibleEventsAsync", WFInstanceId: workflowInstanceId, workflowInstanceId); + + var instance = await _workflowInstanceManager.FindByIdAsync(workflowInstanceId); + if (instance == null) + { + sw.Stop(); + return new WorkflowResponse + { + Status = 1, + Message = $"Workflow instance with id '{workflowInstanceId}' not found", + Data = null + }; + } + + var variables = await _workflowInstanceVariableManager.GetVariablesAsync(workflowInstanceId); + var dict = variables.ToDictionary(a => a.Variable.Name, a => a.Value); + + var model = dict.ConvertToWorkflowModel(); + model.InstanceId = instance.Id; + model.UserId = instance.ExternalUserId; + model.InstanceMode = "exist"; + + var bookmarks = instance.WorkflowState?.Bookmarks ?? new List(); + + var possible = bookmarks + .Select(b => new PossibleEventDto + { + EventName = ExtractEventName(b) ?? "", + BookmarkId = b.Id, + ActivityId = b.ActivityId, + ActivityNodeId = b.ActivityNodeId, + ActivityInstanceId = b.ActivityInstanceId, + CreatedAt = b.CreatedAt, + AutoBurn = b.AutoBurn, + CallbackMethodName = b.CallbackMethodName, + Metadata = b.Metadata + }) + .Where(x => !string.IsNullOrWhiteSpace(x.EventName)) + .GroupBy(x => x.EventName, StringComparer.OrdinalIgnoreCase) + .Select(g => g.First()) + .ToList(); + + // fallback + if (possible.Count == 0 && !string.IsNullOrWhiteSpace(model.NextPageEventName)) + { + possible.Add(new PossibleEventDto + { + EventName = model.NextPageEventName! + }); + } + + model.NextPageData = System.Text.Json.JsonSerializer.SerializeToElement(new { possibleEvents = possible }); + + sw.Stop(); + _logger.Log("End of GetNextPossibleEventsAsync", WFInstanceId: workflowInstanceId, model, sw.ElapsedMilliseconds); + + return new WorkflowResponse + { + Status = 0, + Message = "Success", + Data = model + }; + } + catch (Exception ex) + { + sw.Stop(); + _logger.Log("Exception in GetNextPossibleEventsAsync", WFInstanceId: workflowInstanceId, ex.Message, sw.ElapsedMilliseconds, ex); + + return new WorkflowResponse + { + Status = 100, + Message = ex.Message, + Data = null + }; + } + } + private static string? ExtractEventName(Elsa.Workflows.Models.Bookmark b) + { + // حالت غالب: خود Name همان eventName است + //if (!string.IsNullOrWhiteSpace(b.Name)) + // return b.Name; + //var eventName = ((Elsa.Workflows.Runtime.Stimuli.EventStimulus)b.Payload)?.EventName; + + // اگر جایی Name خالی بود، از Payload هم تلاش می‌کنیم. + var payload = b.Payload; + if (payload == null) return null; + + // اگر payload دیکشنری باشد: + if (payload is IDictionary dict) + { + if (dict.TryGetValue("EventName", out var v) && v is string s1 && !string.IsNullOrWhiteSpace(s1)) return s1; + if (dict.TryGetValue("eventName", out v) && v is string s2 && !string.IsNullOrWhiteSpace(s2)) return s2; + if (dict.TryGetValue("Name", out v) && v is string s3 && !string.IsNullOrWhiteSpace(s3)) return s3; + } + + // اگر payload شیء باشد و property داشته باشد (Reflection سبک): + var t = payload.GetType(); + foreach (var prop in new[] { "EventName", "eventName", "Name" }) + { + var p = t.GetProperty(prop); + if (p?.GetValue(payload) is string s && !string.IsNullOrWhiteSpace(s)) + return s; + } + + return null; + } + //#region EvictDefFromCacheAsync ///// //public async Task> EvictDefFromCacheAsync(string definitionId)