5.3 Pre/Post Images & Organization Service Operations in Plug-ins
Key Takeaways
- Target in InputParameters only contains attributes the caller actually sent; Pre-Images and Post-Images supply the rest of a record's data before and after the core operation.
- A Pre-Image captures a record's values before the operation is applied; a Post-Image captures them after -- Pre-Images aren't meaningful on Create and Post-Images aren't meaningful on Delete.
- Pre-validation steps cannot use entity images at all, since they run before the database transaction opens.
- IOrganizationService, obtained from IOrganizationServiceFactory, is the primary interface for a plug-in to Create, Update, Delete, Retrieve, RetrieveMultiple, or Execute any other SDK message against Dataverse.
- The userId passed to CreateOrganizationService controls whether the service runs as the current user, the initiating user, or with full system privileges.
Not every plug-in that needs data has to query the database for it. Dataverse can hand a plug-in two ready-made snapshots of the record it's operating on -- a Pre-Image and a Post-Image -- and for anything the images don't cover, a plug-in reaches back into Dataverse itself through the Organization service. Both mechanisms exist to solve the same core limitation: InputParameters["Target"] only contains the attributes the caller actually sent, which is frequently not the whole record.
Why Target Alone Isn't Enough
If a user changes only a phone number on an Update, Target contains just telephone1 -- none of the record's other fields are present, because the caller never sent them. A plug-in that needs to compare the new value of a field against its old value, or that needs a field like revenue to run a calculation regardless of what the caller actually edited, cannot get either value from Target alone. That's what entity images solve.
Pre-Images and Post-Images
An entity image is a snapshot of a record's attributes that Dataverse captures and hands to the plug-in alongside the execution context. Images are configured per registered step (in the Plug-in Registration Tool, 5.4), each given a friendly alias -- the string key used to retrieve it in code -- and optionally restricted to a specific set of columns to avoid pulling the whole record.
| Pre-Image | Post-Image | |
|---|---|---|
| Snapshot taken | Before the core operation applies changes | After the core operation applies changes |
| Reflects | The record as it existed in the database prior to this request | The record as it will be saved, including defaulted and calculated fields |
| Valid on | Update, Delete (and other messages with an existing record) | Create, Update |
| Not meaningful on | Create (no "before" record exists) | Delete (no "after" record exists) |
| Available to | Pre-operation and post-operation steps | Pre-operation and post-operation steps |
Note that pre-validation steps cannot use images at all -- pre-validation runs before the database transaction opens, ahead of the point where Dataverse can reliably construct either snapshot.
In code, images arrive as dictionaries on the context, keyed by the alias registered for that step:
Entity preImage = context.PreEntityImages["PreImage"];
Entity postImage = context.PostEntityImages["PostImage"];
decimal oldRevenue = preImage.GetAttributeValue<Money>("revenue")?.Value ?? 0m;
A common exam scenario: "a post-operation plug-in on Update needs to compare a field's old value to its new value." The answer is a Pre-Image registered on that step -- the new value is available directly from Target or the Post-Image, but the old value only exists in the Pre-Image, because Target won't contain it unless the caller happened to resend it.
The Organization Service
For anything beyond what images provide -- querying unrelated tables, writing to other records, calling other messages -- a plug-in uses IOrganizationService, obtained through IOrganizationServiceFactory:
IOrganizationService service = factory.CreateOrganizationService(context.UserId);
The userId argument controls whose security context the service runs under: pass context.UserId to run as the user Dataverse is currently processing the request as, respecting that user's security roles and business unit; pass context.InitiatingUserId to run as whoever originally triggered the request chain; or pass null to run with full system privileges, the equivalent of the SYSTEM account, used sparingly since it bypasses the security-role checks the platform would otherwise enforce.
IOrganizationService exposes the same core operations available anywhere in the SDK:
Create(Entity)-- returns the new record'sGuidUpdate(Entity)-- saves only the attributes present on theEntityobjectDelete(string logicalName, Guid id)Retrieve(string logicalName, Guid id, ColumnSet columns)-- always pass an explicitColumnSet, neverColumnSet(true), so the call doesn't pull every column on the tableRetrieveMultiple(QueryExpression or FetchXML query)-- for querying by criteria rather than by IDExecute(OrganizationRequest request)-- the general-purpose entry point for everything that isn't basic CRUD:WhoAmIRequest,AssignRequest,SetStateRequest, calls to custom APIs (5.5), and any other SDK message
Because a plug-in already runs inside an active transaction, calls made through IOrganizationService from within it participate in that same transaction for synchronous steps -- a later failure and rollback undoes writes the plug-in itself made during the same execution, not just the original triggering operation. That transactional coupling, combined with the Depth counter from 5.2, is exactly why a plug-in that calls Update on the same table it's handling Update for needs to guard against re-entering its own pipeline.
A post-operation plug-in on the Update message needs to know both the old and new value of a field the caller changed. Target contains only the new value the caller submitted. Which image, if registered on the step, supplies the old value?
Match each scenario to the entity image that correctly supplies the needed data:
Match each item on the left with the correct item on the right