diff --git a/src/XrmMockup365/Core.cs b/src/XrmMockup365/Core.cs index bc369704..d221cad5 100644 --- a/src/XrmMockup365/Core.cs +++ b/src/XrmMockup365/Core.cs @@ -32,7 +32,7 @@ namespace DG.Tools.XrmMockup /// /// Class for handling all requests to the database /// - internal class Core : IXrmMockupExtension + internal class Core : IXrmMockupExtension, ICoreOperations { public TimeSpan TimeOffset { get; private set; } public MockupServiceProviderAndFactory ServiceFactory { get; private set; } @@ -54,6 +54,7 @@ internal class Core : IXrmMockupExtension internal OrganizationDetail OrganizationDetail { get; private set; } internal EntityReference BaseCurrency { get; private set; } + private RequestExecutionPipeline pipeline; private PluginManager pluginManager; private CustomApiManager customApiManager; private WorkflowManager workflowManager; @@ -144,14 +145,14 @@ private void InitializeCore(CoreInitializationData initData) } var pluginLogger = loggerFactory.CreateLogger(typeof(PluginManager).FullName); - pluginManager = new PluginManager(initData.Settings.BasePluginTypes, initData.Metadata.EntityMetadata, allPlugins, pluginLogger); + pluginManager = new PluginManager(this, initData.Settings.BasePluginTypes, initData.Metadata.EntityMetadata, allPlugins, pluginLogger); var workflowLogger = loggerFactory.CreateLogger(typeof(WorkflowManager).FullName); - workflowManager = new WorkflowManager(initData.Settings.CodeActivityInstanceTypes, initData.Settings.IncludeAllWorkflows, + workflowManager = new WorkflowManager(this, initData.Settings.CodeActivityInstanceTypes, initData.Settings.IncludeAllWorkflows, initData.Workflows, initData.Metadata.EntityMetadata, workflowLogger); var apiLogger = loggerFactory.CreateLogger(typeof(CustomApiManager).FullName); - customApiManager = new CustomApiManager(initData.Settings.BaseCustomApiTypes, apiLogger); + customApiManager = new CustomApiManager(this, initData.Settings.BaseCustomApiTypes, apiLogger); var typesMissingRegistration = pluginManager.missingRegistrations .Intersect(customApiManager.missingRegistration) @@ -189,6 +190,8 @@ private void InitializeCore(CoreInitializationData initData) security.InitializeSecurityRoles(db); OrganizationDetail = initData.Settings.OrganizationDetail; + pipeline = new RequestExecutionPipeline(this, pluginManager, workflowManager); + FormulaFieldEvaluator = new FormulaFieldEvaluator(ServiceFactory); } @@ -483,7 +486,7 @@ private Entity GetEntity(string entityType) return null; } - internal DbRow GetDbRow(EntityReference entityReference) + public DbRow GetDbRow(EntityReference entityReference) { return db.GetDbRow(entityReference); } @@ -750,287 +753,55 @@ internal void Initialize(params Entity[] entities) /// /// Execute the request and trigger plugins if needed /// - /// - /// - /// public OrganizationResponse Execute(OrganizationRequest request, EntityReference userRef) { return Execute(request, userRef, null); } - internal OrganizationResponse Execute(OrganizationRequest request, EntityReference userRef, + public OrganizationResponse Execute(OrganizationRequest request, EntityReference userRef, PluginContext parentPluginContext) { - // Setup - HandleInternalPreOperations(request, userRef); - - var primaryRef = Mappings.GetPrimaryEntityReferenceFromRequest(request); - - // Create the plugin context - var pluginContext = new PluginContext() - { - UserId = userRef.Id, - InitiatingUserId = userRef.Id, - MessageName = RequestNameToMessageName(request.RequestName), - Depth = 1, - ExtensionDepth = 1, - OrganizationName = OrganizationName, - OrganizationId = OrganizationId, - PrimaryEntityName = primaryRef?.LogicalName, - // IPluginExecutionContext2-7 defaults - IsPortalsClientCall = false, - IsApplicationUser = false, - AuthenticatedUserId = userRef.Id, - }; - if (primaryRef != null) - { - var refEntity = db.GetEntityOrNull(primaryRef); - pluginContext.PrimaryEntityId = refEntity == null ? Guid.Empty : refEntity.Id; - } - - foreach (var prop in request.Parameters) - { - pluginContext.InputParameters[prop.Key] = prop.Value; - } - - if (parentPluginContext != null) - { - pluginContext.ParentContext = parentPluginContext; - pluginContext.Depth = parentPluginContext.Depth + 1; - pluginContext.ExtensionDepth = parentPluginContext.ExtensionDepth + 1; - parentPluginContext.ExtensionDepth = pluginContext.ExtensionDepth; - } - - var buRef = GetBusinessUnit(userRef); - pluginContext.BusinessUnitId = buRef.Id; - - // Get the request message from the mapping, if present, otherwise use the RequestName - var requestMessage = Mappings.RequestToEventOperation.TryGetValue(request.GetType(), out var eventOperation) - ? eventOperation.ToString() - : request.RequestName; - - var entityInfo = GetEntityInfo(request); - - var settings = MockupExecutionContext.GetSettings(request); - // Validation - if (!settings.SetUnsettableFields && (request is UpdateRequest || request is CreateRequest)) - { - var entity = request is UpdateRequest - ? (request as UpdateRequest).Target - : (request as CreateRequest).Target; - Utility.RemoveUnsettableAttributes(request.RequestName, - metadata.EntityMetadata.GetMetadata(entity.LogicalName), entity); - } - - var shouldTrigger = settings.TriggerProcesses && entityInfo != null; - - var entityCollection = entityInfo?.Item1 as EntityCollection; - - Entity preImage = TryRetrieve(primaryRef); - if (preImage != null) - primaryRef.Id = preImage.Id; - - // Populate IPluginExecutionContext4 pre-images collection for Multiple operations - if (entityCollection != null) - { - pluginContext.PreEntityImagesCollection = entityCollection.Entities - .Select(e => { - var img = new EntityImageCollection(); - var pre = e.Id != Guid.Empty ? TryRetrieve(e.ToEntityReference()) : null; - if (pre != null) img["PreImage"] = pre; - return img; - }).ToArray(); - } - - if (shouldTrigger) - { - // System Pre-validation - pluginManager.TriggerSystem(requestMessage, ExecutionStage.PreValidation, entityInfo.Item1, preImage, null, pluginContext, this); - // Pre-validation - pluginManager.TriggerSync(requestMessage, ExecutionStage.PreValidation, entityInfo.Item1, preImage, null, pluginContext, this, (_) => true); - } - - //perform security checks for the request - CheckRequestSecurity(request, userRef); - - //perform initialization of preoperation - InitializePreOperation(request, userRef, preImage); - - if (shouldTrigger) - { - // Shared variables should be moved to parent context when transitioning from 10 to 20. - pluginContext.ParentContext = pluginContext.Clone(); - pluginContext.SharedVariables.Clear(); - - // Pre-operation - pluginManager.TriggerSync(requestMessage, ExecutionStage.PreOperation, entityInfo.Item1, preImage, null, pluginContext, this, (p) => p.GetExecutionOrder() == 0); - - if (settings.TriggerWorkflows) - { - workflowManager.TriggerSync(requestMessage, ExecutionStage.PreOperation, entityInfo.Item1, preImage, null, pluginContext, this); - } - pluginManager.TriggerSync(requestMessage, ExecutionStage.PreOperation, entityInfo.Item1, preImage, null, pluginContext, this, (p) => p.GetExecutionOrder() != 0); - - // System Pre-operation - pluginManager.TriggerSystem(requestMessage, ExecutionStage.PreOperation, entityInfo.Item1, preImage, null, pluginContext, this); - } - - // Core operation - OrganizationResponse response = ExecuteRequest(request, userRef, parentPluginContext, pluginContext); - - // Post-operation - if (shouldTrigger) - { - // In RetrieveMultipleRequests, the OutputParameters bag contains the entity collection - if (request is RetrieveMultipleRequest) - { - pluginContext.OutputParameters["BusinessEntityCollection"] = - (response as RetrieveMultipleResponse)?.EntityCollection; - } - else if (request is RetrieveRequest) - { - pluginContext.OutputParameters["BusinessEntity"] = TryRetrieve((request as RetrieveRequest).Target); - } - - // Populate IPluginExecutionContext4 post-images collection for Multiple operations - if (entityCollection != null) - { - // For CreateMultiple, the original entities may not have their Ids set. - // Use the response Ids (from CreateMultipleResponse) when available. - var responseIds = response.Results.TryGetValue("Ids", out var idsObj) ? idsObj as Guid[] : null; - - pluginContext.PostEntityImagesCollection = entityCollection.Entities - .Select((e, i) => { - var img = new EntityImageCollection(); - var entityId = responseIds != null && i < responseIds.Length ? responseIds[i] : e.Id; - var post = entityId != Guid.Empty - ? TryRetrieve(new EntityReference(entityCollection.EntityName ?? e.LogicalName, entityId)) - : null; - if (post != null) img["PostImage"] = post; - return img; - }).ToArray(); - } - - var syncPostImage = TryRetrieve(primaryRef); - - //copy the createon etc system attributes onto the target so they are available for postoperation processing - if (syncPostImage != null) - { - CopySystemAttributes(syncPostImage, entityInfo.Item1 as Entity); - } - - pluginManager.TriggerSystem(requestMessage, ExecutionStage.PostOperation, entityInfo.Item1, preImage, syncPostImage, pluginContext, this); - - pluginManager.TriggerSync(requestMessage, ExecutionStage.PostOperation, entityInfo.Item1, preImage, syncPostImage, pluginContext, this, (p) => p.GetExecutionOrder() == 0); - - if (settings.TriggerWorkflows) - { - workflowManager.TriggerSync(requestMessage, ExecutionStage.PostOperation, entityInfo.Item1, preImage, syncPostImage, pluginContext, this); - } - - pluginManager.TriggerSync(requestMessage, ExecutionStage.PostOperation, entityInfo.Item1, preImage, syncPostImage, pluginContext, this, (p) => p.GetExecutionOrder() != 0); - - var asyncPostImage = TryRetrieve(primaryRef); - pluginManager.StageAsync(requestMessage, ExecutionStage.PostOperation, entityInfo.Item1, preImage, asyncPostImage, pluginContext, this); - - if (settings.TriggerWorkflows) - { - workflowManager.StageAsync(requestMessage, ExecutionStage.PostOperation, entityInfo.Item1, preImage, asyncPostImage, pluginContext, this); - } - - //When last Sync has been executed we trigger the Async jobs. - if (parentPluginContext == null) - { - pluginManager.TriggerAsyncWaitingJobs(); - - if (settings.TriggerWorkflows) - { - workflowManager.TriggerAsync(this); - } - } + return pipeline.Execute(request, userRef, parentPluginContext); + } - if (settings.TriggerWorkflows) - { - workflowManager.ExecuteWaitingWorkflows(pluginContext, this); - } - } + // ── ICoreOperations — infrastructure methods the pipeline calls back into Core for ── - // Trigger Extension - if (this.settings.MockUpExtensions.Count != 0) - { - /* - * When moving business units, more than eight layers occur... - */ - if (pluginContext.ExtensionDepth > 8) - { - throw new FaultException( - "This workflow job was canceled because the workflow that started it included an infinite loop." + - " Correct the workflow logic and try again."); - } - } - - switch (request.RequestName) - { - case "Create": - var createResponse = (CreateResponse)response; - var entityLogicalName = ((Entity)request.Parameters["Target"]).LogicalName; - var reference = primaryRef ?? new EntityReference(entityLogicalName, createResponse.id); - - var createdEntity = TryRetrieve(reference); - TriggerExtension( - new MockupService(this, userRef.Id, pluginContext), request, - createdEntity, null, userRef); - break; - case "Update": - var updatedEntity = TryRetrieve(primaryRef); - TriggerExtension( - new MockupService(this, userRef.Id, pluginContext), request, - updatedEntity, preImage, userRef); - break; - case "Delete": - TriggerExtension( - new MockupService(this, userRef.Id, pluginContext), request, - null, preImage, userRef); - break; - } + public bool HasMockupExtensions => settings.MockUpExtensions.Count != 0; - return response; + public EntityReference GetBusinessUnit(EntityReference owner) + { + return Utility.GetBusinessUnit(db, owner); } - private void CopySystemAttributes(Entity postImage, Entity item1) + public void CopySystemAttributes(Entity postImage, Entity target) { - if (item1 == null) - { - return; - } + if (target == null) return; - foreach (var systemAttributeName in this.systemAttributeNames) + foreach (var systemAttributeName in systemAttributeNames) { if (postImage.Contains(systemAttributeName)) { if (postImage[systemAttributeName] is EntityReference) { - item1[systemAttributeName] = new EntityReference( + target[systemAttributeName] = new EntityReference( postImage.GetAttributeValue(systemAttributeName).LogicalName, postImage.GetAttributeValue(systemAttributeName).Id); } else if (postImage[systemAttributeName] is DateTime) { - item1[systemAttributeName] = postImage.GetAttributeValue(systemAttributeName); + target[systemAttributeName] = postImage.GetAttributeValue(systemAttributeName); } } } } - internal void HandleInternalPreOperations(OrganizationRequest request, EntityReference userRef) + public void HandleInternalPreOperations(OrganizationRequest request, EntityReference userRef) { if (request.RequestName == "Create") { var entity = request["Target"] as Entity; if (entity.Id == Guid.Empty) - { entity.Id = Guid.NewGuid(); - } if (entity.GetAttributeValue("ownerid") == null && Utility.IsValidAttribute("ownerid", metadata.EntityMetadata.GetMetadata(entity.LogicalName))) @@ -1040,110 +811,44 @@ internal void HandleInternalPreOperations(OrganizationRequest request, EntityRef } } - internal void AddTime(TimeSpan offset) + public OrganizationResponse ExecuteAction(OrganizationRequest request) { - this.TimeOffset = this.TimeOffset.Add(offset); - TriggerWaitingWorkflows(); + return ExecuteActionInternal(request); } - private OrganizationResponse ExecuteRequest( - OrganizationRequest request, - EntityReference userRef, - PluginContext parentPluginContext, - PluginContext pluginContext) - { - if (request is AssignRequest assignRequest) - { - var targetEntity = db.GetEntityOrNull(assignRequest.Target); - if (targetEntity.GetAttributeValue("ownerid") != assignRequest.Assignee) - { - var req = new UpdateRequest - { - Target = new Entity(assignRequest.Target.LogicalName, assignRequest.Target.Id) - }; - req.Target.Attributes["ownerid"] = assignRequest.Assignee; - Execute(req, userRef, parentPluginContext); - } - - return new AssignResponse(); - } - - if (request is SetStateRequest setstateRequest) - { - var targetEntity = db.GetEntityOrNull(setstateRequest.EntityMoniker); - if (targetEntity.GetAttributeValue("statecode") != setstateRequest.State || - targetEntity.GetAttributeValue("statuscode") != setstateRequest.Status) - { - var req = new UpdateRequest - { - Target = new Entity(setstateRequest.EntityMoniker.LogicalName, setstateRequest.EntityMoniker.Id) - }; - req.Target.Attributes["statecode"] = setstateRequest.State; - req.Target.Attributes["statuscode"] = setstateRequest.Status; - Execute(req, userRef, parentPluginContext); - } - - return new SetStateResponse(); - } - - if (workflowManager.GetActionDefaultNull(request.RequestName) != null) - { - return ExecuteAction(request); - } + public bool HandlesCustomApi(string requestName) => customApiManager.HandlesRequest(requestName); - if (customApiManager.HandlesRequest(request.RequestName)) - { - return customApiManager.Execute(request, this, pluginContext); - } - - var handler = RequestHandlers.FirstOrDefault(x => x.HandlesRequest(request.RequestName)); - if (handler != null) - { - return handler.Execute(request, userRef); - } - - if (settings.ExceptionFreeRequests?.Contains(request.RequestName) ?? false) - { - return new OrganizationResponse(); - } + public OrganizationResponse ExecuteCustomApi(OrganizationRequest request, PluginContext pluginContext) + { + return customApiManager.Execute(request, pluginContext); + } - throw new NotImplementedException( - $"Execute for the request '{request.RequestName}' has not been implemented yet."); + public bool IsExceptionFreeRequest(string requestName) + { + return settings.ExceptionFreeRequests?.Contains(requestName) ?? false; } - private void CheckRequestSecurity(OrganizationRequest request, EntityReference userRef) + public IOrganizationService CreateMockupService(Guid? userId, PluginContext pluginContext) { - var handler = RequestHandlers.FirstOrDefault(x => x.HandlesRequest(request.RequestName)); - if (handler != null) - { - handler.CheckSecurity(request, userRef); - } + return new MockupService(this, userId, pluginContext); } - private void InitializePreOperation(OrganizationRequest request, EntityReference userRef, Entity preImage) + public MockupServiceProviderAndFactory CreateServiceProviderAndFactory(PluginContext pluginContext) { - var handler = RequestHandlers.FirstOrDefault(x => x.HandlesRequest(request.RequestName)); - if(handler != null) - { - handler.InitializePreOperation(request, userRef, preImage); - } + return new MockupServiceProviderAndFactory(this, pluginContext, TracingServiceFactory); } - private string RequestNameToMessageName(string requestName) + // ────────────────────────────────────────────────────────────────────────── + + internal void AddTime(TimeSpan offset) { - switch (requestName) - { - case "LoseOpportunity": return "Lose"; - case "WinOpportunity": return "Win"; - case "CloseQuote": return "Lose"; - case "WinQuote": return "Win"; - default: return requestName; - } + this.TimeOffset = this.TimeOffset.Add(offset); + TriggerWaitingWorkflows(); } internal void TriggerWaitingWorkflows() { - workflowManager.ExecuteWaitingWorkflows(null, this); + workflowManager.ExecuteWaitingWorkflows(null); } internal void AddWorkflow(Entity workflow) @@ -1203,7 +908,7 @@ internal bool HasPermission(EntityReference entityRef, AccessRights access, Enti return security.HasPermission(entityRef, access, principleRef); } - private OrganizationResponse ExecuteAction(OrganizationRequest request) + private OrganizationResponse ExecuteActionInternal(OrganizationRequest request) { var action = workflowManager.GetActionDefaultNull(request.RequestName); @@ -1224,7 +929,7 @@ private OrganizationResponse ExecuteAction(OrganizationRequest request) workflow.Variables.Add(argumentName, input.Value); } - var postExecution = workflowManager.ExecuteWorkflow(workflow, entity, null, this); + var postExecution = workflowManager.ExecuteWorkflow(workflow, entity, null); var outputs = workflow.Output.Where(a => postExecution.Variables.ContainsKey(a.Name)) .Select(a => new KeyValuePair(a.Name, postExecution.Variables[a.Name])); @@ -1317,15 +1022,11 @@ private Tuple GetEntityInfo(OrganizationRequest request) } - internal Entity TryRetrieve(EntityReference reference) + public Entity TryRetrieve(EntityReference reference) { return db.GetEntityOrNull(reference)?.CloneEntity(); } - private EntityReference GetBusinessUnit(EntityReference owner) - { - return Utility.GetBusinessUnit(db, owner); - } #endregion internal void DisableRegisteredPlugins(bool include) @@ -1337,7 +1038,7 @@ internal void DisableRegisteredPlugins(bool include) internal List TemporaryPluginRegistrations => pluginManager.TemporaryPluginRegistrations; internal List SystemPluginRegistrations => pluginManager.SystemPluginRegistrations; - internal XrmMockupSettings GetMockupSettings() + public XrmMockupSettings GetMockupSettings() { return settings; } @@ -1424,7 +1125,7 @@ internal void ResetEnvironment() security.ResetEnvironment(db); } - internal EntityMetadata GetEntityMetadata(string entityLogicalName) + public EntityMetadata GetEntityMetadata(string entityLogicalName) { if (!metadata.EntityMetadata.TryGetValue(entityLogicalName, out var entityMetadata)) { diff --git a/src/XrmMockup365/Internal/ExecutionPipelineContext.cs b/src/XrmMockup365/Internal/ExecutionPipelineContext.cs new file mode 100644 index 00000000..0964dabd --- /dev/null +++ b/src/XrmMockup365/Internal/ExecutionPipelineContext.cs @@ -0,0 +1,34 @@ +using Microsoft.Xrm.Sdk; +using System; + +namespace DG.Tools.XrmMockup.Internal +{ + /// + /// Carries all intermediate state for a single request execution through the pipeline stages. + /// Populated progressively: BuildContext → PreValidation → PreOperation → Operation → PostOperation. + /// + internal class ExecutionPipelineContext + { + // Immutable inputs — set once during BuildPipelineContext + public OrganizationRequest Request { get; set; } + public EntityReference UserRef { get; set; } + public PluginContext ParentPluginContext { get; set; } + public MockupServiceSettings Settings { get; set; } + + // Derived during BuildPipelineContext + public PluginContext PluginContext { get; set; } + public string RequestMessage { get; set; } + public Tuple EntityInfo { get; set; } + public EntityReference PrimaryRef { get; set; } + public EntityCollection EntityCollection { get; set; } + public bool ShouldTrigger { get; set; } + + // Images — populated at specific stage boundaries + public Entity PreImage { get; set; } // fetched before PreValidation + public Entity SyncPostImage { get; set; } // fetched at start of PostOperation (sync) + public Entity AsyncPostImage { get; set; } // fetched before async staging + + // Output — set by the main operation stage + public OrganizationResponse Response { get; set; } + } +} diff --git a/src/XrmMockup365/Internal/ICoreOperations.cs b/src/XrmMockup365/Internal/ICoreOperations.cs new file mode 100644 index 00000000..8dc828a9 --- /dev/null +++ b/src/XrmMockup365/Internal/ICoreOperations.cs @@ -0,0 +1,61 @@ +using DG.Tools.XrmMockup.Database; +using Microsoft.Xrm.Sdk; +using Microsoft.Xrm.Sdk.Metadata; +using System; +using System.Collections.Generic; + +namespace DG.Tools.XrmMockup.Internal +{ + /// + /// Infrastructure contract that Core exposes to the pipeline and its managers. + /// The pipeline orchestrates execution stages; Core provides the underlying services. + /// PluginManager, WorkflowManager, and PluginTrigger also depend on this interface + /// so they can be used from the pipeline without a direct Core reference. + /// + internal interface ICoreOperations + { + // Identity + Guid OrganizationId { get; } + string OrganizationName { get; } + + // Time — used by workflow execution and formula fields + TimeSpan TimeOffset { get; } + + // DB helpers + Entity TryRetrieve(EntityReference reference); + DbRow GetDbRow(EntityReference reference); + EntityReference GetBusinessUnit(EntityReference owner); + EntityMetadata GetEntityMetadata(string logicalName); + + // Pre-context setup + void HandleInternalPreOperations(OrganizationRequest request, EntityReference userRef); + + // Post-operation image helper + void CopySystemAttributes(Entity postImage, Entity target); + + // Request handler list — used by pipeline for security check and pre-op init + List RequestHandlers { get; } + + // Recursive entry point for nested requests (Assign → Update, SetState → Update) + OrganizationResponse Execute(OrganizationRequest request, EntityReference userRef, PluginContext parentPluginContext); + + // Dispatch helpers — Core owns the managers so the pipeline delegates through these + OrganizationResponse ExecuteAction(OrganizationRequest request); + bool HandlesCustomApi(string requestName); + OrganizationResponse ExecuteCustomApi(OrganizationRequest request, PluginContext pluginContext); + bool IsExceptionFreeRequest(string requestName); + + // Extension stage support + bool HasMockupExtensions { get; } + IOrganizationService CreateMockupService(Guid? userId, PluginContext pluginContext); + void TriggerExtension(IOrganizationService service, OrganizationRequest request, Entity entity, Entity preImage, EntityReference userRef); + + // Service factories — used by PluginTrigger and WorkflowManager to create execution providers + MockupServiceProviderAndFactory ServiceFactory { get; } + ITracingServiceFactory TracingServiceFactory { get; } + MockupServiceProviderAndFactory CreateServiceProviderAndFactory(PluginContext pluginContext); + + // Settings — used by WorkflowManager + XrmMockupSettings GetMockupSettings(); + } +} diff --git a/src/XrmMockup365/Plugin/CustomApiManager.cs b/src/XrmMockup365/Plugin/CustomApiManager.cs index 572bd390..b8774994 100644 --- a/src/XrmMockup365/Plugin/CustomApiManager.cs +++ b/src/XrmMockup365/Plugin/CustomApiManager.cs @@ -17,6 +17,7 @@ namespace DG.Tools.XrmMockup internal class CustomApiManager { private readonly ILogger _logger; + private readonly Core _core; // Static caches shared across all CustomApiManager instances private static readonly ConcurrentDictionary>> _cachedApis = new ConcurrentDictionary>>(); @@ -30,8 +31,9 @@ internal class CustomApiManager private readonly List> registrationStrategies; - public CustomApiManager(IEnumerable> baseCustomApiTypes, ILogger logger = null) + public CustomApiManager(Core core, IEnumerable> baseCustomApiTypes, ILogger logger = null) { + _core = core; _logger = logger ?? NullLogger.Instance; registrationStrategies = new List> { @@ -142,7 +144,7 @@ public bool HandlesRequest(string requestName) return registeredApis.ContainsKey(requestName); } - internal OrganizationResponse Execute(OrganizationRequest request, Core core, PluginContext pluginContext) + internal OrganizationResponse Execute(OrganizationRequest request, PluginContext pluginContext) { if (!registeredApis.ContainsKey(request.RequestName)) { @@ -153,7 +155,7 @@ internal OrganizationResponse Execute(OrganizationRequest request, Core core, Pl thisPluginContext.Stage = 30; thisPluginContext.Mode = 0; - var serviceProvider = new MockupServiceProviderAndFactory(core, thisPluginContext, core.TracingServiceFactory); + var serviceProvider = new MockupServiceProviderAndFactory(_core, thisPluginContext, _core.TracingServiceFactory); registeredApis[request.RequestName](serviceProvider); pluginContext.OutputParameters.Clear(); diff --git a/src/XrmMockup365/Plugin/PluginManager.cs b/src/XrmMockup365/Plugin/PluginManager.cs index 9ace31a1..b3b0a0a8 100644 --- a/src/XrmMockup365/Plugin/PluginManager.cs +++ b/src/XrmMockup365/Plugin/PluginManager.cs @@ -25,6 +25,7 @@ namespace DG.Tools.XrmMockup internal class PluginManager { private readonly ILogger _logger; + private readonly ICoreOperations _core; // Static caches shared across all PluginManager instances private static readonly ConcurrentDictionary> _cachedRegisteredPlugins = new ConcurrentDictionary>(); @@ -56,8 +57,9 @@ internal class PluginManager private readonly List> registrationStrategies; - public PluginManager(IEnumerable basePluginTypes, Dictionary metadata, List plugins, ILogger logger = null) + public PluginManager(ICoreOperations core, IEnumerable basePluginTypes, Dictionary metadata, List plugins, ILogger logger = null) { + _core = core; _logger = logger ?? NullLogger.Instance; registrationStrategies = new List> { @@ -174,12 +176,20 @@ private void RegisterDirectPlugins(IEnumerable basePluginTypes, Dictionary { if (basePluginTypes == null) return; + var scannedAssemblies = new HashSet(); + foreach (var pluginType in basePluginTypes) { if (pluginType == null) continue; Assembly proxyTypeAssembly = pluginType.Assembly; + if (!scannedAssemblies.Add(proxyTypeAssembly)) + { + _logger.LogDebug("Skipping already-scanned assembly {Assembly}", proxyTypeAssembly.GetName().Name); + continue; + } + _logger.LogDebug("Scanning assembly {Assembly} for direct IPlugin implementations", proxyTypeAssembly.GetName().Name); // Look for any currently loaded types in assembly that implement IPlugin @@ -295,23 +305,26 @@ private static void AddTrigger(PluginTrigger trigger, Dictionary - /// Sorts all the registered which shares the same entry point based on their given order + /// Sorts all the registered which shares the same entry point based on their given order. + /// Uses a stable sort so plugins with equal ExecutionOrder preserve their registration order. /// - private void SortAllLists(Dictionary plugins) + private static void SortAllLists(Dictionary plugins) { foreach (var dictEntry in plugins) { foreach (var listEntry in dictEntry.Value) { - listEntry.Value.Sort(); + var sorted = listEntry.Value.OrderBy(t => t.Order).ToList(); + listEntry.Value.Clear(); + listEntry.Value.AddRange(sorted); } } } public void TriggerSync(string operation, ExecutionStage stage, - object entity, Entity preImage, Entity postImage, PluginContext pluginContext, Core core, Func executionOrderFilter) + object entity, Entity preImage, Entity postImage, PluginContext pluginContext, Func executionOrderFilter) { - TriggerSyncInternal(operation, stage, entity, preImage, postImage, pluginContext, core, executionOrderFilter); + TriggerSyncInternal(operation, stage, entity, preImage, postImage, pluginContext, executionOrderFilter); // Check if this is a Single -> Multiple request var isKnownOp = Enum.TryParse(operation, out var knownOp); @@ -348,7 +361,7 @@ public void TriggerSync(string operation, ExecutionStage stage, }; - TriggerSyncInternal(multipleOperation.ToString(), stage, entityCollection, null, null, multiplePluginContext, core, executionOrderFilter); + TriggerSyncInternal(multipleOperation.ToString(), stage, entityCollection, null, null, multiplePluginContext, executionOrderFilter); } // Check if this is a Multiple -> Single request @@ -394,13 +407,13 @@ public void TriggerSync(string operation, ExecutionStage stage, var entityPostImage = pluginContext.PostEntityImagesCollection.Length > i && pluginContext.PostEntityImagesCollection[i].TryGetValue("PostImage", out var post) ? post : postImage; - TriggerSyncInternal(singleOperation.ToString(), stage, targetEntity, entityPreImage, entityPostImage, singlePluginContext, core, executionOrderFilter); + TriggerSyncInternal(singleOperation.ToString(), stage, targetEntity, entityPreImage, entityPostImage, singlePluginContext, executionOrderFilter); } } } private void TriggerSyncInternal(EventOperation operation, ExecutionStage stage, - object entity, Entity preImage, Entity postImage, PluginContext pluginContext, Core core, Func executionOrderFilter) + object entity, Entity preImage, Entity postImage, PluginContext pluginContext, Func executionOrderFilter) { if (!disableRegisteredPlugins && registeredPlugins.TryGetValue(operation, out var operationPlugins) && operationPlugins.TryGetValue(stage, out var stagePlugins)) stagePlugins @@ -408,7 +421,7 @@ private void TriggerSyncInternal(EventOperation operation, ExecutionStage stage, .Where(executionOrderFilter) .OrderBy(p => p.GetExecutionOrder()) .ToList() - .ForEach(p => p.ExecuteIfMatch(entity, preImage, postImage, pluginContext, core)); + .ForEach(p => p.ExecuteIfMatch(entity, preImage, postImage, pluginContext, _core)); if (temporaryPlugins.TryGetValue(operation, out var tempOperationPlugins) && tempOperationPlugins.TryGetValue(stage, out var tempStagePlugins)) tempStagePlugins @@ -416,17 +429,17 @@ private void TriggerSyncInternal(EventOperation operation, ExecutionStage stage, .Where(executionOrderFilter) .OrderBy(p => p.GetExecutionOrder()) .ToList() - .ForEach(p => p.ExecuteIfMatch(entity, preImage, postImage, pluginContext, core)); + .ForEach(p => p.ExecuteIfMatch(entity, preImage, postImage, pluginContext, _core)); } public void StageAsync(EventOperation operation, ExecutionStage stage, - object entity, Entity preImage, Entity postImage, PluginContext pluginContext, Core core) + object entity, Entity preImage, Entity postImage, PluginContext pluginContext) { if (!disableRegisteredPlugins && registeredPlugins.TryGetValue(operation, out var operationPlugins) && operationPlugins.TryGetValue(stage, out var stagePlugins)) stagePlugins .Where(p => p.GetExecutionMode() == ExecutionMode.Asynchronous) .OrderBy(p => p.GetExecutionOrder()) - .Select(p => p.ToPluginExecution(entity, preImage, postImage, pluginContext, core)) + .Select(p => p.ToPluginExecution(entity, preImage, postImage, pluginContext, _core)) .ToList() .ForEach(pendingAsyncPlugins.Enqueue); @@ -434,7 +447,7 @@ public void StageAsync(EventOperation operation, ExecutionStage stage, tempStagePlugins .Where(p => p.GetExecutionMode() == ExecutionMode.Asynchronous) .OrderBy(p => p.GetExecutionOrder()) - .Select(p => p.ToPluginExecution(entity, preImage, postImage, pluginContext, core)) + .Select(p => p.ToPluginExecution(entity, preImage, postImage, pluginContext, _core)) .ToList() .ForEach(pendingAsyncPlugins.Enqueue); } @@ -448,7 +461,7 @@ public void TriggerAsyncWaitingJobs() } public void TriggerSystem(EventOperation operation, ExecutionStage stage, - object entity, Entity preImage, Entity postImage, PluginContext pluginContext, Core core) + object entity, Entity preImage, Entity postImage, PluginContext pluginContext) { if (!registeredSystemPlugins.TryGetValue(operation, out var stagePlugins)) { @@ -460,7 +473,7 @@ public void TriggerSystem(EventOperation operation, ExecutionStage stage, return; } - plugins.ForEach(p => p.ExecuteIfMatch(entity, preImage, postImage, pluginContext, core)); + plugins.ForEach(p => p.ExecuteIfMatch(entity, preImage, postImage, pluginContext, _core)); } private string GeneratePluginCacheKey(IEnumerable basePluginTypes) diff --git a/src/XrmMockup365/Plugin/PluginTrigger.cs b/src/XrmMockup365/Plugin/PluginTrigger.cs index 6e2db774..4eb7ef37 100644 --- a/src/XrmMockup365/Plugin/PluginTrigger.cs +++ b/src/XrmMockup365/Plugin/PluginTrigger.cs @@ -63,7 +63,7 @@ public int GetExecutionOrder() } // Saves "execution" for Async plugins to be executed after sync plugins. - public PluginExecutionProvider ToPluginExecution(object entityObject, Entity preImage, Entity postImage, PluginContext pluginContext, Core core) + public PluginExecutionProvider ToPluginExecution(object entityObject, Entity preImage, Entity postImage, PluginContext pluginContext, ICoreOperations core) { var entity = entityObject as Entity; var entityRef = entityObject as EntityReference; @@ -75,13 +75,13 @@ public PluginExecutionProvider ToPluginExecution(object entityObject, Entity pre { // Create the plugin context var thisPluginContext = CreatePluginContext(pluginContext, guid, logicalName, preImage, postImage); - return new PluginExecutionProvider(PluginExecute, new MockupServiceProviderAndFactory(core, thisPluginContext, core.TracingServiceFactory)); + return new PluginExecutionProvider(PluginExecute, core.CreateServiceProviderAndFactory(thisPluginContext)); } return null; } - public void ExecuteIfMatch(object entityObject, Entity preImage, Entity postImage, PluginContext pluginContext, Core core) + public void ExecuteIfMatch(object entityObject, Entity preImage, Entity postImage, PluginContext pluginContext, ICoreOperations core) { // Check if it is supposed to execute. Returns preemptively, if it should not. var entity = entityObject as Entity; @@ -106,7 +106,7 @@ public void ExecuteIfMatch(object entityObject, Entity preImage, Entity postImag var thisPluginContext = CreatePluginContext(pluginContext, guid, logicalName, preImage, postImage); //Create Serviceprovider, and execute plugin - MockupServiceProviderAndFactory provider = new MockupServiceProviderAndFactory(core, thisPluginContext, core.TracingServiceFactory); + MockupServiceProviderAndFactory provider = core.CreateServiceProviderAndFactory(thisPluginContext); try { PluginExecute(provider); diff --git a/src/XrmMockup365/RequestExecutionPipeline.cs b/src/XrmMockup365/RequestExecutionPipeline.cs new file mode 100644 index 00000000..324c7888 --- /dev/null +++ b/src/XrmMockup365/RequestExecutionPipeline.cs @@ -0,0 +1,430 @@ +using DG.Tools.XrmMockup.Internal; +using XrmPluginCore.Enums; +using Microsoft.Crm.Sdk.Messages; +using Microsoft.Xrm.Sdk; +using Microsoft.Xrm.Sdk.Messages; +using Microsoft.Xrm.Sdk.Query; +using System; +using System.Linq; +using System.ServiceModel; + +namespace DG.Tools.XrmMockup +{ + /// + /// Orchestrates the Dataverse request execution pipeline across its six stages: + /// Setup → PreValidation → PreOperation → MainOperation → PostOperation → Extensions. + /// + /// Core provides infrastructure (DB, security, extensions). + /// PluginManager and WorkflowManager drive what fires at each stage. + /// + internal class RequestExecutionPipeline + { + private readonly ICoreOperations core; + private readonly PluginManager pluginManager; + private readonly WorkflowManager workflowManager; + + public RequestExecutionPipeline(ICoreOperations core, PluginManager pluginManager, WorkflowManager workflowManager) + { + this.core = core; + this.pluginManager = pluginManager; + this.workflowManager = workflowManager; + } + + public OrganizationResponse Execute(OrganizationRequest request, EntityReference userRef, + PluginContext parentPluginContext) + { + var ctx = BuildPipelineContext(request, userRef, parentPluginContext); + ExecutePreValidationStage(ctx); + ExecuteSecurityAndInit(ctx); + ExecutePreOperationStage(ctx); + ctx.Response = DispatchRequest(ctx); + ExecutePostOperationStage(ctx); + ExecuteExtensionStage(ctx); + return ctx.Response; + } + + // ── Stage 1: build context ──────────────────────────────────────────────── + + private ExecutionPipelineContext BuildPipelineContext(OrganizationRequest request, EntityReference userRef, + PluginContext parentPluginContext) + { + core.HandleInternalPreOperations(request, userRef); + + var primaryRef = Mappings.GetPrimaryEntityReferenceFromRequest(request); + + var pluginContext = new PluginContext() + { + UserId = userRef.Id, + InitiatingUserId = userRef.Id, + MessageName = RequestNameToMessageName(request.RequestName), + Depth = 1, + ExtensionDepth = 1, + OrganizationName = core.OrganizationName, + OrganizationId = core.OrganizationId, + PrimaryEntityName = primaryRef?.LogicalName, + IsPortalsClientCall = false, + IsApplicationUser = false, + AuthenticatedUserId = userRef.Id, + }; + + if (primaryRef != null) + { + var existingEntity = core.TryRetrieve(primaryRef); + pluginContext.PrimaryEntityId = existingEntity == null ? Guid.Empty : existingEntity.Id; + } + + foreach (var prop in request.Parameters) + pluginContext.InputParameters[prop.Key] = prop.Value; + + if (parentPluginContext != null) + { + pluginContext.ParentContext = parentPluginContext; + pluginContext.Depth = parentPluginContext.Depth + 1; + pluginContext.ExtensionDepth = parentPluginContext.ExtensionDepth + 1; + parentPluginContext.ExtensionDepth = pluginContext.ExtensionDepth; + } + + pluginContext.BusinessUnitId = core.GetBusinessUnit(userRef).Id; + + var requestMessage = Mappings.RequestToEventOperation.TryGetValue(request.GetType(), out var eventOperation) + ? eventOperation.ToString() + : request.RequestName; + + var entityInfo = GetEntityInfo(request); + + var requestSettings = MockupExecutionContext.GetSettings(request); + if (!requestSettings.SetUnsettableFields && (request is UpdateRequest || request is CreateRequest)) + { + var entity = request is UpdateRequest + ? (request as UpdateRequest).Target + : (request as CreateRequest).Target; + Utility.RemoveUnsettableAttributes(request.RequestName, + core.GetEntityMetadata(entity.LogicalName), entity); + } + + var entityCollection = entityInfo?.Item1 as EntityCollection; + + var preImage = core.TryRetrieve(primaryRef); + if (preImage != null) + primaryRef.Id = preImage.Id; + + // Populate IPluginExecutionContext4 pre-images collection for Multiple operations + if (entityCollection != null) + { + pluginContext.PreEntityImagesCollection = entityCollection.Entities + .Select(e => + { + var img = new EntityImageCollection(); + var pre = e.Id != Guid.Empty ? core.TryRetrieve(e.ToEntityReference()) : null; + if (pre != null) img["PreImage"] = pre; + return img; + }).ToArray(); + } + + return new ExecutionPipelineContext + { + Request = request, + UserRef = userRef, + ParentPluginContext = parentPluginContext, + Settings = requestSettings, + PluginContext = pluginContext, + RequestMessage = requestMessage, + EntityInfo = entityInfo, + PrimaryRef = primaryRef, + EntityCollection = entityCollection, + ShouldTrigger = requestSettings.TriggerProcesses && entityInfo != null, + PreImage = preImage, + }; + } + + // ── Stage 2: PreValidation (stage 10) ──────────────────────────────────── + + private void ExecutePreValidationStage(ExecutionPipelineContext ctx) + { + if (!ctx.ShouldTrigger) return; + + pluginManager.TriggerSystem(ctx.RequestMessage, ExecutionStage.PreValidation, + ctx.EntityInfo.Item1, ctx.PreImage, null, ctx.PluginContext); + pluginManager.TriggerSync(ctx.RequestMessage, ExecutionStage.PreValidation, + ctx.EntityInfo.Item1, ctx.PreImage, null, ctx.PluginContext, (_) => true); + } + + // ── Stage 3: security check + attribute initialisation ─────────────────── + + private void ExecuteSecurityAndInit(ExecutionPipelineContext ctx) + { + var handler = core.RequestHandlers.FirstOrDefault(x => x.HandlesRequest(ctx.Request.RequestName)); + handler?.CheckSecurity(ctx.Request, ctx.UserRef); + handler?.InitializePreOperation(ctx.Request, ctx.UserRef, ctx.PreImage); + } + + // ── Stage 4: PreOperation (stage 20) ───────────────────────────────────── + + private void ExecutePreOperationStage(ExecutionPipelineContext ctx) + { + if (!ctx.ShouldTrigger) return; + + // Shared variables move to the parent context when transitioning stage 10 → stage 20 + ctx.PluginContext.ParentContext = ctx.PluginContext.Clone(); + ctx.PluginContext.SharedVariables.Clear(); + + pluginManager.TriggerSync(ctx.RequestMessage, ExecutionStage.PreOperation, + ctx.EntityInfo.Item1, ctx.PreImage, null, ctx.PluginContext, + p => p.GetExecutionOrder() == 0); + + if (ctx.Settings.TriggerWorkflows) + workflowManager.TriggerSync(ctx.RequestMessage, ExecutionStage.PreOperation, + ctx.EntityInfo.Item1, ctx.PreImage, null, ctx.PluginContext); + + pluginManager.TriggerSync(ctx.RequestMessage, ExecutionStage.PreOperation, + ctx.EntityInfo.Item1, ctx.PreImage, null, ctx.PluginContext, + p => p.GetExecutionOrder() != 0); + + pluginManager.TriggerSystem(ctx.RequestMessage, ExecutionStage.PreOperation, + ctx.EntityInfo.Item1, ctx.PreImage, null, ctx.PluginContext); + } + + // ── Stage 5: main operation (stage 30) ─────────────────────────────────── + + private OrganizationResponse DispatchRequest(ExecutionPipelineContext ctx) + { + var request = ctx.Request; + var userRef = ctx.UserRef; + + if (request is AssignRequest assignRequest) + { + var targetEntity = core.TryRetrieve(assignRequest.Target); + if (targetEntity?.GetAttributeValue("ownerid") != assignRequest.Assignee) + { + var req = new UpdateRequest + { + Target = new Entity(assignRequest.Target.LogicalName, assignRequest.Target.Id) + }; + req.Target.Attributes["ownerid"] = assignRequest.Assignee; + core.Execute(req, userRef, ctx.ParentPluginContext); + } + return new AssignResponse(); + } + + if (request is SetStateRequest setstateRequest) + { + var targetEntity = core.TryRetrieve(setstateRequest.EntityMoniker); + if (targetEntity?.GetAttributeValue("statecode") != setstateRequest.State || + targetEntity?.GetAttributeValue("statuscode") != setstateRequest.Status) + { + var req = new UpdateRequest + { + Target = new Entity(setstateRequest.EntityMoniker.LogicalName, setstateRequest.EntityMoniker.Id) + }; + req.Target.Attributes["statecode"] = setstateRequest.State; + req.Target.Attributes["statuscode"] = setstateRequest.Status; + core.Execute(req, userRef, ctx.ParentPluginContext); + } + return new SetStateResponse(); + } + + if (workflowManager.GetActionDefaultNull(request.RequestName) != null) + return core.ExecuteAction(request); + + if (core.HandlesCustomApi(request.RequestName)) + return core.ExecuteCustomApi(request, ctx.PluginContext); + + var handler = core.RequestHandlers.FirstOrDefault(x => x.HandlesRequest(request.RequestName)); + if (handler != null) + return handler.Execute(request, userRef); + + if (core.IsExceptionFreeRequest(request.RequestName)) + return new OrganizationResponse(); + + throw new NotImplementedException( + $"Execute for the request '{request.RequestName}' has not been implemented yet."); + } + + // ── Stage 6: PostOperation (stage 40) ──────────────────────────────────── + + private void ExecutePostOperationStage(ExecutionPipelineContext ctx) + { + if (!ctx.ShouldTrigger) return; + + // Populate OutputParameters for retrieve operations so plugins can read them + if (ctx.Request is RetrieveMultipleRequest) + { + ctx.PluginContext.OutputParameters["BusinessEntityCollection"] = + (ctx.Response as RetrieveMultipleResponse)?.EntityCollection; + } + else if (ctx.Request is RetrieveRequest) + { + ctx.PluginContext.OutputParameters["BusinessEntity"] = + core.TryRetrieve((ctx.Request as RetrieveRequest).Target); + } + + // Populate IPluginExecutionContext4 post-images collection for Multiple operations + if (ctx.EntityCollection != null) + { + // For CreateMultiple, the original entities may not have their Ids set. + // Use the response Ids (from CreateMultipleResponse) when available. + var responseIds = ctx.Response.Results.TryGetValue("Ids", out var idsObj) + ? idsObj as Guid[] + : null; + + ctx.PluginContext.PostEntityImagesCollection = ctx.EntityCollection.Entities + .Select((e, i) => + { + var img = new EntityImageCollection(); + var entityId = responseIds != null && i < responseIds.Length ? responseIds[i] : e.Id; + var post = entityId != Guid.Empty + ? core.TryRetrieve(new EntityReference(ctx.EntityCollection.EntityName ?? e.LogicalName, entityId)) + : null; + if (post != null) img["PostImage"] = post; + return img; + }).ToArray(); + } + + ctx.SyncPostImage = core.TryRetrieve(ctx.PrimaryRef); + if (ctx.SyncPostImage != null) + core.CopySystemAttributes(ctx.SyncPostImage, ctx.EntityInfo.Item1 as Entity); + + // Sync post-operation: system first, then user plugins ordered by ExecutionOrder, interleaved with workflows + pluginManager.TriggerSystem(ctx.RequestMessage, ExecutionStage.PostOperation, + ctx.EntityInfo.Item1, ctx.PreImage, ctx.SyncPostImage, ctx.PluginContext); + + pluginManager.TriggerSync(ctx.RequestMessage, ExecutionStage.PostOperation, + ctx.EntityInfo.Item1, ctx.PreImage, ctx.SyncPostImage, ctx.PluginContext, + p => p.GetExecutionOrder() == 0); + + if (ctx.Settings.TriggerWorkflows) + workflowManager.TriggerSync(ctx.RequestMessage, ExecutionStage.PostOperation, + ctx.EntityInfo.Item1, ctx.PreImage, ctx.SyncPostImage, ctx.PluginContext); + + pluginManager.TriggerSync(ctx.RequestMessage, ExecutionStage.PostOperation, + ctx.EntityInfo.Item1, ctx.PreImage, ctx.SyncPostImage, ctx.PluginContext, + p => p.GetExecutionOrder() != 0); + + // Stage async work — re-fetch post-image so async jobs see the final committed state + ctx.AsyncPostImage = core.TryRetrieve(ctx.PrimaryRef); + pluginManager.StageAsync(ctx.RequestMessage, ExecutionStage.PostOperation, + ctx.EntityInfo.Item1, ctx.PreImage, ctx.AsyncPostImage, ctx.PluginContext); + + if (ctx.Settings.TriggerWorkflows) + workflowManager.StageAsync(ctx.RequestMessage, ExecutionStage.PostOperation, + ctx.EntityInfo.Item1, ctx.PreImage, ctx.AsyncPostImage, ctx.PluginContext); + + // Async jobs only fire at the top-level call, not from within a plugin + if (ctx.ParentPluginContext == null) + { + pluginManager.TriggerAsyncWaitingJobs(); + + if (ctx.Settings.TriggerWorkflows) + workflowManager.TriggerAsync(); + } + + if (ctx.Settings.TriggerWorkflows) + workflowManager.ExecuteWaitingWorkflows(ctx.PluginContext); + } + + // ── Stage 7: MockupExtensions ───────────────────────────────────────────── + + private void ExecuteExtensionStage(ExecutionPipelineContext ctx) + { + if (!core.HasMockupExtensions) return; + + // Guard against infinite recursion when moving business units (8+ layers can occur) + if (ctx.PluginContext.ExtensionDepth > 8) + { + throw new FaultException( + "This workflow job was canceled because the workflow that started it included an infinite loop." + + " Correct the workflow logic and try again."); + } + + var service = core.CreateMockupService(ctx.UserRef.Id, ctx.PluginContext); + switch (ctx.Request.RequestName) + { + case "Create": + var createResponse = (CreateResponse)ctx.Response; + var entityLogicalName = ((Entity)ctx.Request.Parameters["Target"]).LogicalName; + var reference = ctx.PrimaryRef ?? new EntityReference(entityLogicalName, createResponse.id); + core.TriggerExtension(service, ctx.Request, core.TryRetrieve(reference), null, ctx.UserRef); + break; + case "Update": + core.TriggerExtension(service, ctx.Request, core.TryRetrieve(ctx.PrimaryRef), ctx.PreImage, ctx.UserRef); + break; + case "Delete": + core.TriggerExtension(service, ctx.Request, null, ctx.PreImage, ctx.UserRef); + break; + } + } + + // ── Pure helpers ────────────────────────────────────────────────────────── + + private static string RequestNameToMessageName(string requestName) + { + switch (requestName) + { + case "LoseOpportunity": return "Lose"; + case "WinOpportunity": return "Win"; + case "CloseQuote": return "Lose"; + case "WinQuote": return "Win"; + default: return requestName; + } + } + + private static Tuple GetEntityInfo(OrganizationRequest request) + { + Mappings.EntityImageProperty.TryGetValue(request.GetType(), out string key); + object obj = null; + if (key != null) + obj = request.Parameters[key]; + + if (request is WinOpportunityRequest || request is LoseOpportunityRequest) + { + var close = request is WinOpportunityRequest + ? (request as WinOpportunityRequest).OpportunityClose + : (request as LoseOpportunityRequest).OpportunityClose; + obj = close.GetAttributeValue("opportunityid"); + } + else if (request is WinQuoteRequest || request is CloseQuoteRequest) + { + var close = request is WinQuoteRequest + ? (request as WinQuoteRequest).QuoteClose + : (request as CloseQuoteRequest).QuoteClose; + obj = close.GetAttributeValue("quoteid"); + } + else if (request is CloseIncidentRequest closeIncidentRequest) + { + obj = closeIncidentRequest.IncidentResolution?.GetAttributeValue("incidentid"); + } + else if (request is RetrieveMultipleRequest retrieveMultiple) + { + string entityName = null; + switch (retrieveMultiple.Query) + { + case FetchExpression fe: + entityName = XmlHandling.FetchXmlToQueryExpression(fe.Query).EntityName; + break; + case QueryExpression query: + entityName = query.EntityName; + break; + case QueryByAttribute qba: + entityName = qba.EntityName; + break; + } + + if (entityName != null) + return new Tuple( + new EntityReference { LogicalName = entityName, Id = Guid.Empty }, + entityName, Guid.Empty); + } + + if (obj is Entity entity) + return new Tuple(obj, entity.LogicalName, entity.Id); + + if (obj is EntityReference entityRef) + return new Tuple(obj, entityRef.LogicalName, entityRef.Id); + + if (obj is EntityCollection entityCollection) + return new Tuple(obj, entityCollection.EntityName, Guid.Empty); + + return null; + } + } +} diff --git a/src/XrmMockup365/Workflow/WorkflowManager.cs b/src/XrmMockup365/Workflow/WorkflowManager.cs index 0e829321..2963afc2 100644 --- a/src/XrmMockup365/Workflow/WorkflowManager.cs +++ b/src/XrmMockup365/Workflow/WorkflowManager.cs @@ -25,6 +25,7 @@ namespace DG.Tools.XrmMockup { internal class WorkflowManager { private readonly ILogger _logger; + private readonly ICoreOperations _core; // Static caches shared across all WorkflowManager instances private static readonly ConcurrentDictionary _staticParsedWorkflows = new ConcurrentDictionary(); @@ -46,7 +47,8 @@ internal class WorkflowManager { internal int ActionsCount => actions.Count; internal int CodeActivityCount => codeActivityTriggers.Count; - public WorkflowManager(IEnumerable codeActivityInstances, bool? IncludeAllWorkflows, List mixedWorkflows, Dictionary metadata, ILogger logger = null) { + public WorkflowManager(ICoreOperations core, IEnumerable codeActivityInstances, bool? IncludeAllWorkflows, List mixedWorkflows, Dictionary metadata, ILogger logger = null) { + _core = core; _logger = logger ?? NullLogger.Instance; this.metadata = metadata; this.actions = mixedWorkflows.Where(w => w.GetAttributeValue("category").Value == 3).ToList(); @@ -105,23 +107,23 @@ public WorkflowManager(IEnumerable codeActivityInstances, bool? IncludeAll /// /// public void TriggerSync(string operation, ExecutionStage stage, - object entity, Entity preImage, Entity postImage, PluginContext pluginContext, Core core) + object entity, Entity preImage, Entity postImage, PluginContext pluginContext) { var toExecute = synchronousWorkflows.Where(x => ShouldExecute(x, operation, stage, entity, pluginContext)).ToList(); foreach (var workflow in toExecute) { - Execute(workflow, operation, entity, preImage, postImage, pluginContext, core); + Execute(workflow, operation, entity, preImage, postImage, pluginContext); } } - public void TriggerAsync(Core core) + public void TriggerAsync() { while (pendingAsyncWorkflows.TryDequeue(out var workflowContext)) { - Entity primaryEntity = core.GetDbRow(workflowContext.primaryRef).ToEntity(); + Entity primaryEntity = _core.GetDbRow(workflowContext.primaryRef).ToEntity(); - var execution = ExecuteWorkflow(workflowContext.workflow, primaryEntity, workflowContext.pluginContext, core); + var execution = ExecuteWorkflow(workflowContext.workflow, primaryEntity, workflowContext.pluginContext); if (execution.Variables["Wait"] != null) { @@ -131,7 +133,7 @@ public void TriggerAsync(Core core) } public void StageAsync(string operation, ExecutionStage stage, - object entity, Entity preImage, Entity postImage, PluginContext pluginContext, Core core) + object entity, Entity preImage, Entity postImage, PluginContext pluginContext) { var toExecute = asynchronousWorkflows.Where(x => ShouldStage(x, operation, stage, entity, pluginContext)).ToList(); foreach (var workflow in toExecute) @@ -140,18 +142,18 @@ public void StageAsync(string operation, ExecutionStage stage, } } - internal void ExecuteWaitingWorkflows(PluginContext pluginContext, Core core) { - var provider = new MockupServiceProviderAndFactory(core, pluginContext, core.TracingServiceFactory); - var triggerWorkflows = core.GetMockupSettings().TriggerWorkflows ?? true; + internal void ExecuteWaitingWorkflows(PluginContext pluginContext) { + var provider = _core.CreateServiceProviderAndFactory(pluginContext); + var triggerWorkflows = _core.GetMockupSettings().TriggerWorkflows ?? true; var service = provider.CreateOrganizationService(null, new MockupServiceSettings(true, triggerWorkflows, true, MockupServiceSettings.Role.SDK)); foreach (var waitInfo in waitingWorkflows.ToList()) { waitingWorkflows.Remove(waitInfo); var variables = waitInfo.VariablesInstance; - var shadowService = core.ServiceFactory.CreateAdminOrganizationService(new MockupServiceSettings(false, true, MockupServiceSettings.Role.SDK)); + var shadowService = _core.ServiceFactory.CreateAdminOrganizationService(new MockupServiceSettings(false, true, MockupServiceSettings.Role.SDK)); var primaryEntity = shadowService.Retrieve(waitInfo.PrimaryEntity.LogicalName, waitInfo.PrimaryEntity.Id, new ColumnSet(true)); variables["InputEntities(\"primaryEntity\")"] = primaryEntity; - waitInfo.Element.Execute(waitInfo.ElementIndex, ref variables, core.TimeOffset, service, provider, provider.GetService()); + waitInfo.Element.Execute(waitInfo.ElementIndex, ref variables, _core.TimeOffset, service, provider, provider.GetService()); if (variables["Wait"] != null) { waitingWorkflows.Add(variables["Wait"] as WaitInfo); } @@ -352,7 +354,7 @@ private bool ShouldExecute(Entity workflow, string operation, ExecutionStage sta return true; } - private void Execute(Entity workflow, string operation, object entityObject, Entity preImage, Entity postImage, PluginContext pluginContext, Core core) + private void Execute(Entity workflow, string operation, object entityObject, Entity preImage, Entity postImage, PluginContext pluginContext) { // Check if it is supposed to execute. Returns preemptively, if it should not. var entity = entityObject as Entity; @@ -391,11 +393,11 @@ private void Execute(Entity workflow, string operation, object entityObject, Ent WorkflowTree postExecution = null; if (thisStage == workflow_stage.Preoperation) { - postExecution = ExecuteWorkflow(parsedWorkflow, preImage.CloneEntity(), thisPluginContext, core); + postExecution = ExecuteWorkflow(parsedWorkflow, preImage.CloneEntity(), thisPluginContext); } else { - postExecution = ExecuteWorkflow(parsedWorkflow, postImage.CloneEntity(), thisPluginContext, core); + postExecution = ExecuteWorkflow(parsedWorkflow, postImage.CloneEntity(), thisPluginContext); } if (postExecution.Variables["Wait"] != null) @@ -404,11 +406,11 @@ private void Execute(Entity workflow, string operation, object entityObject, Ent } } - internal WorkflowTree ExecuteWorkflow(WorkflowTree workflow, Entity primaryEntity, PluginContext pluginContext, Core core) { - var provider = new MockupServiceProviderAndFactory(core, pluginContext, core.TracingServiceFactory); - var triggerWorkflows = core.GetMockupSettings().TriggerWorkflows ?? true; + internal WorkflowTree ExecuteWorkflow(WorkflowTree workflow, Entity primaryEntity, PluginContext pluginContext) { + var provider = _core.CreateServiceProviderAndFactory(pluginContext); + var triggerWorkflows = _core.GetMockupSettings().TriggerWorkflows ?? true; var service = provider.CreateAdminOrganizationService(new MockupServiceSettings(true, triggerWorkflows, true, MockupServiceSettings.Role.SDK)); - return workflow.Execute(primaryEntity, core.TimeOffset, service, provider, provider.GetService()); + return workflow.Execute(primaryEntity, _core.TimeOffset, service, provider, provider.GetService()); } internal WorkflowTree ParseWorkflow(Entity workflow) {