Skip to content

Commit e6d4aa1

Browse files
authored
Merge pull request #322 from delegateas/feature/311-nullreferenceexception-when-update-plugi
Fix NullReferenceException when plugins with post-images fire via Multiple requests
2 parents 4793843 + c43a480 commit e6d4aa1

5 files changed

Lines changed: 124 additions & 3 deletions

File tree

src/XrmMockup365/Core.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1316,7 +1316,7 @@ private Tuple<object, string, Guid> GetEntityInfo(OrganizationRequest request)
13161316
}
13171317

13181318

1319-
private Entity TryRetrieve(EntityReference reference)
1319+
internal Entity TryRetrieve(EntityReference reference)
13201320
{
13211321
return db.GetEntityOrNull(reference)?.CloneEntity();
13221322
}

src/XrmMockup365/Plugin/PluginManager.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,7 @@ public void TriggerSync(string operation, ExecutionStage stage,
347347
postImage != null ? new EntityImageCollection { { "PostImage", postImage } } : new EntityImageCollection()
348348
};
349349

350+
350351
TriggerSyncInternal(multipleOperation.ToString(), stage, entityCollection, null, null, multiplePluginContext, core, executionOrderFilter);
351352
}
352353

src/XrmMockup365/Plugin/PluginTrigger.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,8 @@ private void CheckSpecialRequest()
144144

145145
private Entity AddPostImageAttributesToEntity(Entity entity, Entity preImage, Entity postImage)
146146
{
147-
if (Operation.Matches(EventOperation.Update) && Stage == ExecutionStage.PostOperation)
147+
if (Operation.Matches(EventOperation.Update) && Stage == ExecutionStage.PostOperation
148+
&& preImage != null && postImage != null)
148149
{
149150
var shadowAddedAttributes = postImage.Attributes.Where(a => !preImage.Attributes.ContainsKey(a.Key) && !entity.Attributes.ContainsKey(a.Key));
150151
entity = entity.CloneEntity();
@@ -215,7 +216,7 @@ private PluginContext CreatePluginContext(PluginContext pluginContext, Guid guid
215216
{
216217
thisPluginContext.PostEntityImages.Add(image.ImageName, postImage.CloneEntity(Metadata.GetMetadata(postImage.LogicalName), cols));
217218
}
218-
if (preImage != null && imageType == ImageType.PreImage || imageType == ImageType.Both)
219+
if (preImage != null && (imageType == ImageType.PreImage || imageType == ImageType.Both))
219220
{
220221
thisPluginContext.PreEntityImages.Add(image.ImageName, preImage.CloneEntity(Metadata.GetMetadata(preImage.LogicalName), cols));
221222
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
using DG.XrmFramework.BusinessDomain.ServiceContext;
2+
using XrmPluginCore;
3+
using XrmPluginCore.Enums;
4+
using Microsoft.Xrm.Sdk;
5+
6+
namespace DG.Some.Namespace
7+
{
8+
public class ContactPostImageOnUpdatePlugin : Plugin
9+
{
10+
public ContactPostImageOnUpdatePlugin()
11+
{
12+
#pragma warning disable CS0618 // Type or member is obsolete - disabled for testing purposes
13+
RegisterPluginStep<Contact>(
14+
EventOperation.Update,
15+
ExecutionStage.PostOperation,
16+
Execute)
17+
.AddFilteredAttributes(c => c.Description)
18+
.WithPostImage();
19+
#pragma warning restore CS0618
20+
}
21+
22+
protected void Execute(LocalPluginContext localContext)
23+
{
24+
// Access post-images to verify they don't throw NullReferenceException
25+
var postImages = localContext.PluginExecutionContext.PostEntityImages;
26+
localContext.PluginExecutionContext.SharedVariables["PostImageCount"] = postImages.Count;
27+
}
28+
}
29+
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
using System.Linq;
2+
using Microsoft.Xrm.Sdk;
3+
using Microsoft.Xrm.Sdk.Messages;
4+
using Microsoft.Xrm.Sdk.Query;
5+
using DG.XrmFramework.BusinessDomain.ServiceContext;
6+
using Xunit;
7+
8+
namespace DG.XrmMockupTest
9+
{
10+
public class TestMultipleRequestPluginImages : UnitTestBase
11+
{
12+
public TestMultipleRequestPluginImages(XrmMockupFixture fixture) : base(fixture) { }
13+
14+
[Fact]
15+
public void UpdateMultiple_TriggersUpdatePostOpPlugin_WithPostImages()
16+
{
17+
// Arrange: create contacts
18+
var contact1Id = orgGodService.Create(new Contact { FirstName = "Alice", LastName = "One" });
19+
var contact2Id = orgGodService.Create(new Contact { FirstName = "Bob", LastName = "Two" });
20+
21+
// Act: UpdateMultiple - this previously threw NullReferenceException
22+
// because plugins with post-images received null images from the Multiple->Single cross-trigger
23+
var updateMultiple = new UpdateMultipleRequest
24+
{
25+
Targets = new EntityCollection
26+
{
27+
EntityName = Contact.EntityLogicalName,
28+
Entities =
29+
{
30+
new Contact(contact1Id) { Description = "updated-1" },
31+
new Contact(contact2Id) { Description = "updated-2" },
32+
}
33+
}
34+
};
35+
36+
// Should not throw NullReferenceException
37+
orgAdminService.Execute(updateMultiple);
38+
39+
// Assert: entities were updated
40+
var retrieved1 = Contact.Retrieve(orgAdminService, contact1Id);
41+
var retrieved2 = Contact.Retrieve(orgAdminService, contact2Id);
42+
Assert.Equal("updated-1", retrieved1.Description);
43+
Assert.Equal("updated-2", retrieved2.Description);
44+
}
45+
46+
[Fact]
47+
public void CreateMultiple_DoesNotCrash()
48+
{
49+
var createMultiple = new CreateMultipleRequest
50+
{
51+
Targets = new EntityCollection
52+
{
53+
EntityName = Contact.EntityLogicalName,
54+
Entities =
55+
{
56+
new Contact { FirstName = "Charlie", LastName = "Three" },
57+
new Contact { FirstName = "Diana", LastName = "Four" },
58+
}
59+
}
60+
};
61+
62+
// Should not throw
63+
var response = (CreateMultipleResponse)orgAdminService.Execute(createMultiple);
64+
Assert.NotNull(response);
65+
}
66+
67+
[Fact]
68+
public void SingleUpdate_TriggersUpdateMultiplePlugin()
69+
{
70+
// The SetCityOnCreateUpdateMultiple plugin is registered on UpdateMultiple/PreOperation
71+
// and should fire via the Single->Multiple cross-trigger
72+
var contactId = orgGodService.Create(new Contact { FirstName = "Eve", Address2_City = "Berlin" });
73+
74+
orgAdminService.Update(new Contact(contactId) { FirstName = "Eve-Updated" });
75+
76+
var retrieved = Contact.Retrieve(orgAdminService, contactId);
77+
Assert.Equal("Copenhagen", retrieved.Address2_City);
78+
}
79+
80+
[Fact]
81+
public void SingleCreate_TriggersCreateMultiplePlugin()
82+
{
83+
// The SetCityOnCreateUpdateMultiple plugin is registered on CreateMultiple/PreOperation
84+
var contactId = orgAdminService.Create(new Contact { FirstName = "Frank" });
85+
86+
var retrieved = Contact.Retrieve(orgAdminService, contactId);
87+
Assert.Equal("Copenhagen", retrieved.Address2_City);
88+
}
89+
}
90+
}

0 commit comments

Comments
 (0)