DIGITALL Assembly (Digitall.Plugins)¶
Digitall.Plugins is the
abstraction layer every plugin, Custom API handler, and workflow activity is built on. It
replaces boilerplate IServiceProvider plumbing with a small, consistent API surface, and is a
hard dependency for registration attributes and
server-side unit testing to work the way this
guideline describes.
Migrating from dgt.apower
Versions before 2.0.0 shipped as package dgt.apower with namespace Digitall.APower,
plus separate dgt.apower.keyvault/dgt.apower.sharepoint/dgt.apower.enviromentvariables
add-on packages. As of 2.0.0 the package was renamed to Digitall.Plugins (package ID
now matches the namespace), the Key Vault and SharePoint modules were removed, and
environment-variable access moved into the core package — see
Add-on modules below.
PluginSkeleton vs. Executor
The package's own README now lists PluginSkeleton first and labels Executor a
"legacy-compatible base class" — PluginSkeleton is the maintainers' recommended starting
point for new plugins. This guideline still teaches Executor below because most other
server-side pages (Custom API, Patterns,
Testing) build on its typed-property surface, and
Executor remains fully supported, not deprecated. If you're starting a new project and
don't need that typed surface, prefer PluginSkeleton (see the tip below) — the choice
doesn't affect registration attributes or dgtp push, which
work the same either way.
Executor — the base class for your plugin¶
Inherit from Executor and implement Execute():
using Digitall.Plugins;
using Digitall.Dataverse.Model; // generated early-bound model
namespace dgt.Plugin.Contacts;
public class ContactValidationPlugin : Executor
{
protected override ExecutionResult Execute()
{
var contact = Entity.ToEntity<Contact>();
if (!contact.BirthDate.HasValue)
{
return ExecutionResult.Skipped;
}
// ... your logic, using the typed members below ...
return ExecutionResult.Ok;
}
}
DGT-SRV-080 — Execute() returns an ExecutionResult — Ok, Failure, or Skipped —
rather than relying purely on exceptions for control flow. This is also what
Digitall.Dataverse.Testing asserts against in
unit tests, so use it deliberately rather than always returning Ok.
Executor.Execute(IServiceProvider) (the actual IPlugin.Execute) clones itself before running
your code — following Microsoft's "plugins must be stateless" guidance — so instance fields
never leak state between invocations of the same registered step. static fields are not
protected by this; keep them out of plugins entirely (see
DGT-SRV-130).
Prefer a lighter base, or already have a plain IPlugin? Use PluginSkeleton
PluginSkeleton is the package's recommended abstract IPlugin base for new plugins — it
wraps your logic with structured start/end/failure logging and timing (auto-logged to both
PluginTelemetry and ITracingService) built in, while leaving you working against the raw
IServiceProvider/IPluginExecutionContext inside ExecuteInternal(IServiceProvider) —
typed access comes from extension methods such as executionContext.GetTarget<T>(),
GetPreImage<T>()/GetPostImage<T>(), GetInputParameter/SetOutputParameter, and
serviceProvider.GetOrganizationService()/GetElevatedOrganizationService()/GetLogger()/
GetManagedIdentityService()/GetTimeProvider(). Also useful when a class already derives
from another base or you're retrofitting an existing plugin — see
examples/SamplePlugin/SkeletonSample.cs in the AssemblyPower repo for a full walkthrough.
Executor (documented on this page) stays a fully-supported alternative: it adds
clone-per-execution behavior and the ExecutionResult contract on top of the same
capabilities, exposed as properties instead of extension methods — and it's what the rest of
this guideline's server-side examples use.
What Executor gives you, without touching IServiceProvider directly¶
| Member | Purpose |
|---|---|
Entity / EntityReference |
The Target input parameter, typed for you. |
PreEntityImage / PostEntityImage |
The "PreImage"/"PostImage" entity images — named to match what registration attributes register them as. |
SecuredOrganizationService |
IOrganizationService running as the calling user. |
ElevatedOrganizationService |
IOrganizationService running with elevated (system) privileges. |
Stage / Mode / Depth |
Pipeline stage, sync/async mode, and recursion depth as readable values instead of raw integers. |
GetInputParameter<T> / GetOutputParameter<T> / SetOutputParameter<T> |
Generic, typed access to the message request/response parameters — used for Custom APIs and actions. |
Query(out QueryExpression, out ColumnSet) (+ QueryByAttribute/FetchExpression overloads) |
Typed access to a Custom API's Query input, with column set inference. |
Core |
The raw IPluginExecutionContext, for anything not surfaced as a typed property above. |
ServiceProvider |
The raw IServiceProvider — the gateway to GetTracingService(), GetLogger(), and the other extension methods. |
ProcessName |
A ready-made "CRM.{Type}.{Message}.{Mode}.{Stage}.{Depth}" string for logging/telemetry correlation. |
Executor itself doesn't wrap a Trace(...) convenience method — call
ServiceProvider.GetTracingService() or ServiceProvider.GetLogger() directly (see
Logging below) for step-by-step diagnostic output.
DGT-SRV-090 — Prefer SecuredOrganizationService/ElevatedOrganizationService over calling
OrganizationService(...) directly — the two named properties are cached per execution and
communicate intent (running as caller vs. running elevated) at the call site.
Comparing target vs. pre-image¶
executor.GetEntityAttributeValue<T>(attribute), IsEntityAttributeValueNew(attribute),
IsEntityAttributeValueChanged<T>(attribute), IsEntityAttributeValueNullOrEmpty(attribute),
and MergeEntity<T>() (also available on the raw IPluginExecutionContext) cover the common
"what actually changed" checks against Entity/PreEntityImage without writing the
Contains/null-checking boilerplate by hand — see
src/Digitall.Plugins/Extensions/EntityAttributeExtension.cs in the AssemblyPower repo for the
exact semantics of each.
Add-on modules¶
As of 2.0.0, Digitall.Plugins is a single package — the separate Key Vault and
SharePoint add-on modules from the dgt.apower.* era have been removed outright (no REST-based
KeyVaultSecret(url) or ISharepointService helpers exist anymore); if you still need Key
Vault or SharePoint access, call the respective SDKs/REST APIs directly.
Environment-variable access is no longer a separate add-on — it's built into the core package:
var value = executor.GetConfig("dgt_some_env"); // Executor, via extension method
var value = serviceProvider.GetConfig("dgt_some_env"); // raw IServiceProvider (e.g. PluginSkeleton)
GetConfig reads an environment variable's value (definition + value lookup, elevated,
defaultvalue fallback) without hand-rolling the environmentvariabledefinition/
environmentvariablevalue query yourself — see
src/Digitall.Plugins/Extensions/EnvironmentVariablesExtension.cs in the AssemblyPower repo.
Telemetry is not a separate module either — ServiceProvider (on both Executor and a raw
IServiceProvider) exposes two logging entry points, deliberately kept distinct:
| Call | Returns | Writes to |
|---|---|---|
GetLogger() |
Microsoft.Xrm.Sdk.PluginTelemetry.ILogger |
Application Insights PluginTelemetry only |
GetLogger(params LogSink[] sinks) |
Microsoft.Extensions.Logging.ILogger |
PluginTelemetry, ITracingService, or both, depending on the sinks passed — no sinks defaults to ITracingService |
Prefer the standard Microsoft.Extensions.Logging.ILogger overload (GetLogger(sinks)) for new
code — it lets you change sinks later without touching call sites, and it's what
PluginSkeleton (see the tip above) uses internally to auto-log start/end/failure around every
execution.
Custom APIs and workflow activities¶
The same Executor base (or PluginSkeleton/extension-method surface) applies to Custom API
handlers and workflow activities, not just classic plugins — see
Custom API & Data Providers for the
Custom-API-specific parts (input/output parameter typing via GetInputParameter/
SetOutputParameter, and the [CustomApiRegistration] attribute).