Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
393 changes: 47 additions & 346 deletions src/XrmMockup365/Core.cs

Large diffs are not rendered by default.

34 changes: 34 additions & 0 deletions src/XrmMockup365/Internal/ExecutionPipelineContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
using Microsoft.Xrm.Sdk;
using System;

namespace DG.Tools.XrmMockup.Internal
{
/// <summary>
/// Carries all intermediate state for a single request execution through the pipeline stages.
/// Populated progressively: BuildContext → PreValidation → PreOperation → Operation → PostOperation.
/// </summary>
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<object, string, Guid> 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; }
}
}
61 changes: 61 additions & 0 deletions src/XrmMockup365/Internal/ICoreOperations.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// 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.
/// </summary>
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<RequestHandler> 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();
}
}
8 changes: 5 additions & 3 deletions src/XrmMockup365/Plugin/CustomApiManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, Dictionary<string, Action<MockupServiceProviderAndFactory>>> _cachedApis = new ConcurrentDictionary<string, Dictionary<string, Action<MockupServiceProviderAndFactory>>>();
Expand All @@ -30,8 +31,9 @@ internal class CustomApiManager

private readonly List<IRegistrationStrategy<ICustomApiConfig>> registrationStrategies;

public CustomApiManager(IEnumerable<Tuple<string, Type>> baseCustomApiTypes, ILogger logger = null)
public CustomApiManager(Core core, IEnumerable<Tuple<string, Type>> baseCustomApiTypes, ILogger logger = null)
{
_core = core;
_logger = logger ?? NullLogger.Instance;
registrationStrategies = new List<IRegistrationStrategy<ICustomApiConfig>>
{
Expand Down Expand Up @@ -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))
{
Expand All @@ -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();
Expand Down
45 changes: 29 additions & 16 deletions src/XrmMockup365/Plugin/PluginManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, Dictionary<EventOperation, StageToTriggerMap>> _cachedRegisteredPlugins = new ConcurrentDictionary<string, Dictionary<EventOperation, StageToTriggerMap>>();
Expand Down Expand Up @@ -56,8 +57,9 @@ internal class PluginManager

private readonly List<IRegistrationStrategy<IPluginStepConfig>> registrationStrategies;

public PluginManager(IEnumerable<Type> basePluginTypes, Dictionary<string, EntityMetadata> metadata, List<MetaPlugin> plugins, ILogger logger = null)
public PluginManager(ICoreOperations core, IEnumerable<Type> basePluginTypes, Dictionary<string, EntityMetadata> metadata, List<MetaPlugin> plugins, ILogger logger = null)
{
_core = core;
_logger = logger ?? NullLogger.Instance;
registrationStrategies = new List<IRegistrationStrategy<IPluginStepConfig>>
{
Expand Down Expand Up @@ -174,12 +176,20 @@ private void RegisterDirectPlugins(IEnumerable<Type> basePluginTypes, Dictionary
{
if (basePluginTypes == null) return;

var scannedAssemblies = new HashSet<Assembly>();

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
Expand Down Expand Up @@ -295,23 +305,26 @@ private static void AddTrigger(PluginTrigger trigger, Dictionary<EventOperation,
}

/// <summary>
/// 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.
/// </summary>
Comment thread
mkholt marked this conversation as resolved.
private void SortAllLists(Dictionary<EventOperation, StageToTriggerMap> plugins)
private static void SortAllLists(Dictionary<EventOperation, StageToTriggerMap> 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<PluginTrigger, bool> executionOrderFilter)
object entity, Entity preImage, Entity postImage, PluginContext pluginContext, Func<PluginTrigger, bool> 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<EventOperationEnum>(operation, out var knownOp);
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -394,47 +407,47 @@ 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<PluginTrigger, bool> executionOrderFilter)
object entity, Entity preImage, Entity postImage, PluginContext pluginContext, Func<PluginTrigger, bool> executionOrderFilter)
{
if (!disableRegisteredPlugins && registeredPlugins.TryGetValue(operation, out var operationPlugins) && operationPlugins.TryGetValue(stage, out var stagePlugins))
stagePlugins
.Where(p => p.GetExecutionMode() == ExecutionMode.Synchronous)
.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
.Where(p => p.GetExecutionMode() == ExecutionMode.Synchronous)
.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);

if (temporaryPlugins.TryGetValue(operation, out var tempOperationPlugins) && tempOperationPlugins.TryGetValue(stage, out var tempStagePlugins))
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);
}
Expand All @@ -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))
{
Expand All @@ -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<Type> basePluginTypes)
Expand Down
8 changes: 4 additions & 4 deletions src/XrmMockup365/Plugin/PluginTrigger.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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);
Expand Down
Loading
Loading