Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
4 changes: 2 additions & 2 deletions src/XrmMockup365/Core.cs
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ private void InitializeCore(CoreInitializationData initData)
}

sw.Stop();
coreLogger.LogInformation("XrmMockup initialized in {ElapsedMs}ms Plugins: {PluginCount}, Workflows: sync={SyncCount} async={AsyncCount}, Custom APIs: {ApiCount}",
coreLogger.LogInformation("XrmMockup initialized in {ElapsedMs}ms - Plugins: {PluginCount}, Workflows: sync={SyncCount} async={AsyncCount}, Custom APIs: {ApiCount}",
sw.ElapsedMilliseconds,
pluginManager.PluginRegistrations.Count,
workflowManager.SynchronousWorkflowCount,
Expand Down Expand Up @@ -1279,7 +1279,7 @@ private Tuple<object, string, Guid> GetEntityInfo(OrganizationRequest request)
}


private Entity TryRetrieve(EntityReference reference)
internal Entity TryRetrieve(EntityReference reference)
{
return db.GetEntityOrNull(reference)?.CloneEntity();
}
Expand Down
25 changes: 21 additions & 4 deletions src/XrmMockup365/Plugin/PluginManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -339,7 +339,8 @@ public void TriggerSync(string operation, ExecutionStage stage,
? request.RequestName
: throw new MockupException($"Could not create request for operation {operation}");

// TODO: Images for multiple are handled in IPluginExecutionContext4
// Images for Multiple operations are accessed via IPluginExecutionContext4.PreEntityImagesCollection/PostEntityImagesCollection,
// not via PreEntityImages/PostEntityImages. Pass null until IPluginExecutionContext4 is implemented.
TriggerSyncInternal(multipleOperation.ToString(), stage, entityCollection, null, null, multiplePluginContext, core, executionOrderFilter);
}

Expand Down Expand Up @@ -378,9 +379,25 @@ public void TriggerSync(string operation, ExecutionStage stage,
var singlePluginContext = pluginContext.Clone();
singlePluginContext.InputParameters[singleImageProperty] = targetEntity;
singlePluginContext.MessageName = singleMessageName;

// TODO: Recalculate preImage and postImage here
TriggerSyncInternal(singleOperation.ToString(), stage, targetEntity, preImage, postImage, singlePluginContext, core, executionOrderFilter);

var entityRef = targetEntity.ToEntityReference();
var currentImage = core.TryRetrieve(entityRef);

Entity singlePreImage;
Entity singlePostImage;
if (stage == ExecutionStage.PostOperation)
{
singlePreImage = preImage;
singlePostImage = currentImage;
}
else
{
singlePreImage = currentImage;
singlePostImage = null;
}

TriggerSyncInternal(singleOperation.ToString(), stage, targetEntity,
singlePreImage, singlePostImage, singlePluginContext, core, executionOrderFilter);
}
}
}
Expand Down
5 changes: 3 additions & 2 deletions src/XrmMockup365/Plugin/PluginTrigger.cs
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,8 @@ private void CheckSpecialRequest()

private Entity AddPostImageAttributesToEntity(Entity entity, Entity preImage, Entity postImage)
{
if (Operation.Matches(EventOperation.Update) && Stage == ExecutionStage.PostOperation)
if (Operation.Matches(EventOperation.Update) && Stage == ExecutionStage.PostOperation
&& preImage != null && postImage != null)
{
var shadowAddedAttributes = postImage.Attributes.Where(a => !preImage.Attributes.ContainsKey(a.Key) && !entity.Attributes.ContainsKey(a.Key));
entity = entity.CloneEntity();
Expand Down Expand Up @@ -215,7 +216,7 @@ private PluginContext CreatePluginContext(PluginContext pluginContext, Guid guid
{
thisPluginContext.PostEntityImages.Add(image.ImageName, postImage.CloneEntity(Metadata.GetMetadata(postImage.LogicalName), cols));
}
if (preImage != null && imageType == ImageType.PreImage || imageType == ImageType.Both)
if (preImage != null && (imageType == ImageType.PreImage || imageType == ImageType.Both))
{
thisPluginContext.PreEntityImages.Add(image.ImageName, preImage.CloneEntity(Metadata.GetMetadata(preImage.LogicalName), cols));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
using System;
using DG.XrmFramework.BusinessDomain.ServiceContext;
using XrmPluginCore;
using XrmPluginCore.Enums;
using Microsoft.Xrm.Sdk;

namespace DG.Some.Namespace
{
public class ContactPostImageOnUpdatePlugin : Plugin
{
public ContactPostImageOnUpdatePlugin()
{
RegisterStep<Contact>(
EventOperation.Update,
ExecutionStage.PostOperation,
Execute)
.AddFilteredAttributes(c => c.Description)
.WithPostImage();
}

protected void Execute(LocalPluginContext localContext)
{
if (localContext == null)
{
throw new ArgumentNullException(nameof(localContext));
}

var target = localContext.PluginExecutionContext.InputParameters["Target"] as Entity;
if (target == null)
{
throw new InvalidPluginExecutionException("Target is null.");
}

var postImages = localContext.PluginExecutionContext.PostEntityImages;

var service = localContext.OrganizationService;
service.Create(new Task
{
Subject = $"PostImagePlugin executed. HasPostImage={postImages.Count > 0}",
RegardingObjectId = new EntityReference(Contact.EntityLogicalName, target.Id),
});
}
}
}
99 changes: 99 additions & 0 deletions tests/XrmMockup365Test/TestMultipleRequestPluginImages.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
using System.Linq;
using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Messages;
using Microsoft.Xrm.Sdk.Query;
using DG.XrmFramework.BusinessDomain.ServiceContext;
using Xunit;

namespace DG.XrmMockupTest
{
public class TestMultipleRequestPluginImages : UnitTestBase
{
public TestMultipleRequestPluginImages(XrmMockupFixture fixture) : base(fixture) { }

[Fact]
public void UpdateMultiple_TriggersUpdatePostOpPlugin_WithPostImages()
{
// Arrange: create contacts
var contact1Id = orgGodService.Create(new Contact { FirstName = "Alice", LastName = "One" });
var contact2Id = orgGodService.Create(new Contact { FirstName = "Bob", LastName = "Two" });

// Act: UpdateMultiple - this previously threw NullReferenceException
var updateMultiple = new UpdateMultipleRequest
{
Targets = new EntityCollection
{
EntityName = Contact.EntityLogicalName,
Entities =
{
new Contact(contact1Id) { Description = "updated-1" },
new Contact(contact2Id) { Description = "updated-2" },
}
}
};
orgAdminService.Execute(updateMultiple);

// Assert: the PostImagePlugin should have created a Task for each contact
var query = new QueryExpression("task") { ColumnSet = new ColumnSet(true) };
var tasks = orgAdminService.RetrieveMultiple(query);

var pluginTasks = tasks.Entities
.Where(t => t.GetAttributeValue<string>("subject")?.Contains("PostImagePlugin executed") == true)
.ToList();

Assert.True(pluginTasks.Count >= 2, $"Expected at least 2 plugin tasks, but found {pluginTasks.Count}");
Assert.All(pluginTasks, t => Assert.Contains("HasPostImage=True", t.GetAttributeValue<string>("subject")));
}

[Fact]
public void CreateMultiple_TriggersCreatePostOpPlugin_DoesNotCrash()
{
// Arrange & Act: CreateMultiple should not crash even though
// there's a PostImage plugin registered on Update (not Create)
var createMultiple = new CreateMultipleRequest
{
Targets = new EntityCollection
{
EntityName = Contact.EntityLogicalName,
Entities =
{
new Contact { FirstName = "Charlie", LastName = "Three" },
new Contact { FirstName = "Diana", LastName = "Four" },
}
}
};

// Should not throw
var response = (CreateMultipleResponse)orgAdminService.Execute(createMultiple);
Assert.NotNull(response);
}

[Fact]
public void SingleUpdate_TriggersUpdateMultiplePlugin_DoesNotCrash()
{
// Arrange: create a contact, then do a single Update
// The SetCityOnCreateUpdateMultiple plugin is registered on UpdateMultiple
// and should fire via the Single->Multiple cross-trigger
var contactId = orgGodService.Create(new Contact { FirstName = "Eve", Address2_City = "Berlin" });

// Act: single Update triggers cross-fire to UpdateMultiple
orgAdminService.Update(new Contact(contactId) { FirstName = "Eve-Updated" });

// Assert: the UpdateMultiple plugin should have set city to Copenhagen
var retrieved = Contact.Retrieve(orgAdminService, contactId);
Assert.Equal("Copenhagen", retrieved.Address2_City);
}

[Fact]
public void SingleCreate_TriggersCreateMultiplePlugin_DoesNotCrash()
{
// Act: single Create triggers cross-fire to CreateMultiple
// The SetCityOnCreateUpdateMultiple plugin is registered on CreateMultiple
var contactId = orgAdminService.Create(new Contact { FirstName = "Frank" });

// Assert: the CreateMultiple plugin should have set city to Copenhagen
var retrieved = Contact.Retrieve(orgAdminService, contactId);
Assert.Equal("Copenhagen", retrieved.Address2_City);
}
}
}
Loading