NuGet package for Microsoft Dataverse plugin development that stays close to the standard IPlugin model while adding focused convenience features (extensions, logging adapters, and optional base classes).
- Installation
- Ecosystem Synergy
- Design Goals
- Quick Start
- Usage
- API Reference
- Examples
- Requirements
- License
dotnet add package Digitall.PluginsFor a project in a subdirectory:
dotnet add src/MyProject package Digitall.PluginsDigitall.Plugins is designed to work well on its own, but the best end-to-end developer experience comes from combining it with the related DIGITALL packages/tools:
| Purpose | Package / Tool | Link |
|---|---|---|
| Plugin implementation helpers (base classes + extensions) | Digitall.Plugins |
https://www.nuget.org/packages/Digitall.Plugins |
| Fast, in-memory Dataverse testing | Digitall.Dataverse.Testing |
https://www.nuget.org/packages/Digitall.Dataverse.Testing |
| Attribute-based plugin/workflow registration metadata | Digitall.Plugins.Registration |
https://www.nuget.org/packages/Digitall.Plugins.Registration |
| ALM automation, registration and model generation workflows | dgt.power (dgtp) |
https://www.nuget.org/packages/dgt.power |
Typical flow:
- Implement plugin logic with
PluginSkeleton(or plainIPlugin) plus extension methods from this package. - Add registration attributes via
Digitall.Plugins.Registration. - Use
dgt.power(dgtp) to handle registration and early-bound model generation workflows. - Test plugin behavior quickly with
Digitall.Dataverse.Testing, including deterministic time-based tests viaTimeProvider.
- Stay close to Microsoft's standard Dataverse plugin model (
IPlugin+IServiceProvider) to keep behavior predictable. - Keep framework logic transparent: extension methods provide convenience without adding hidden pipeline behavior.
- Let teams adopt incrementally: use pure
IPlugin,PluginSkeleton, orExecutordepending on project needs.
Choose the base class that best fits your plugin design. For new plugins, start with PluginSkeleton. If you prefer plain IPlugin, the extension methods work there as well.
using Digitall.Plugins;
using Microsoft.Xrm.Sdk;
public class MyPlugin : PluginSkeleton
{
protected override void ExecuteInternal(IServiceProvider serviceProvider)
{
var context = serviceProvider.GetExecutionContext();
var entity = context.GetTarget<Entity>();
// Your plugin logic here
}
}using Digitall.Plugins;
public class MyExecutor : Executor
{
protected override ExecutionResult Execute()
{
var entity = Entity; // Pre-unwrapped from context
var id = entity.Id;
return ExecutionResult.Ok;
}
}PluginSkeleton is the recommended base class for new plugins. It follows Microsoft's stateless plugin guidance while keeping the standard IServiceProvider programming model you already know from raw IPlugin.
Benefits:
- Same
IServiceProviderflow as regularIPlugin, so existing code patterns are easy to migrate. - Designed for stateless execution (important because Dataverse may reuse plugin instances across invocations).
- Automatic execution start/end logging with elapsed time.
- Structured exception logging and rethrow behavior.
- Access to
Microsoft.Extensions.Logging.ILogger-compatible logging via selectable sinks.
Important
Keep plugin implementations stateless. Avoid mutable instance properties for request-specific data because the Dataverse runtime may reuse plugin instances across executions.
Override abstract method:
protected abstract void ExecuteInternal(IServiceProvider serviceProvider);Example:
using Digitall.Plugins;
using Digitall.Plugins.Extensions;
using Microsoft.Xrm.Sdk;
public class AccountPlugin : PluginSkeleton
{
protected override void ExecuteInternal(IServiceProvider serviceProvider)
{
var context = serviceProvider.GetExecutionContext();
var account = context.GetTarget<Entity>();
// Access services via extensions
var logger = serviceProvider.GetLogger(
ServiceProviderExtensions.LogSink.PluginTelemetry,
ServiceProviderExtensions.LogSink.TracingService
);
var service = serviceProvider.GetOrganizationService();
logger.LogInformation("Processing account: {0}", account.Id);
}
}Executor is a legacy-compatible base class kept for backward compatibility. It is not deprecated, but new plugins should generally start with PluginSkeleton (or plain IPlugin if preferred).
Benefits:
- Simplified property access (Entity, EntityReference, PreEntityImage, etc.)
- Pre-configured OrganizationService properties (Secured/Elevated)
- Support for secure and unsecure configuration
- ExecutionResult tracking
- Support for CreateMultiple/UpdateMultiple operations
Constructor parameters (optional):
| Parameter | Type | Description |
|---|---|---|
unsecure |
string |
Unsecure configuration string |
secure |
string |
Secure configuration string |
Common Properties:
| Property | Type | Description |
|---|---|---|
Entity |
Entity |
The target entity for single-record operations |
GetEntities() |
EntityCollection |
Target entities for CreateMultiple/UpdateMultiple |
EntityReference |
EntityReference |
The target entity reference |
PreEntityImage |
Entity |
Pre-operation entity image |
GetPreEntityImages() |
EntityCollection |
Pre-operation entity images (for bulk operations) |
PostEntityImage |
Entity |
Post-operation entity image |
GetPostEntityImages() |
EntityCollection |
Post-operation entity images (for bulk operations) |
SecuredOrganizationService |
IOrganizationService |
Organization service with caller's privileges |
ElevatedOrganizationService |
IOrganizationService |
Organization service with system privileges |
Core |
IPluginExecutionContext |
The raw execution context |
Example:
using Digitall.Plugins;
public class AccountExecutor : Executor
{
protected override ExecutionResult Execute()
{
var entity = Entity;
var accountName = entity.GetAttributeValue<string>("name");
// Use pre-configured services
var service = SecuredOrganizationService;
return ExecutionResult.Ok;
}
}Extension methods are the core value of this library. They work with any plugin style (IPlugin, PluginSkeleton, Executor, or custom base classes) and add convenience APIs without introducing black-box framework behavior.
using Digitall.Plugins.Extensions;
using Microsoft.Xrm.Sdk;
var context = serviceProvider.GetExecutionContext();
var service = serviceProvider.GetOrganizationService();
var elevated = serviceProvider.GetElevatedOrganizationService();
var logger = serviceProvider.GetLogger();
var tracing = serviceProvider.GetTracingService();
var timeProvider = serviceProvider.GetTimeProvider();
var miService = serviceProvider.GetManagedIdentityService();Available Methods:
| Method | Returns | Description |
|---|---|---|
GetExecutionContext() |
IPluginExecutionContext7 |
The plugin execution context |
GetOrganizationService() |
IOrganizationService |
Service for current user (from context) |
GetOrganizationService(Guid userId) |
IOrganizationService |
Service for specified user |
GetElevatedOrganizationService() |
IOrganizationService |
Service with system (elevated) privileges |
GetTracingService() |
ITracingService |
Tracing service for legacy logging |
GetLogger() |
Microsoft.Xrm.Sdk.PluginTelemetry.ILogger |
Native Dataverse Plugin Telemetry logger |
GetLogger(params LogSink[] sinks) |
Microsoft.Extensions.Logging.ILogger |
Standard .NET logger abstraction with selectable sinks |
GetTimeProvider() |
TimeProvider |
Current time provider (testable) |
GetManagedIdentityService() |
IManagedIdentityService |
Managed identity service for token acquisition |
Use TimeProvider for date/time-dependent plugin logic instead of directly calling DateTime.UtcNow.
At runtime, GetTimeProvider() falls back to TimeProvider.System when no custom provider is registered by the host. In tests, inject a fake provider (for example FakeTimeProvider from Microsoft.Extensions.TimeProvider.Testing), optionally together with Digitall.Dataverse.Testing for full in-memory Dataverse test scenarios.
var timeProvider = serviceProvider.GetTimeProvider();
var utcNow = timeProvider.GetUtcNow();The library supports two logger interfaces and keeps the naming explicit:
GetLogger()returnsMicrosoft.Xrm.Sdk.PluginTelemetry.ILogger(Dataverse plugin telemetry logger).GetLogger(params LogSink[] sinks)returnsMicrosoft.Extensions.Logging.ILogger(standard .NET logging abstraction).- Older versions exposed
LoggingFacade; current guidance is to use the standard .NETILoggerabstraction fromGetLogger(params LogSink[] sinks).
This lets you code against the standard .NET abstraction and change log sinks without rewriting logging calls (for example from TracingService only to TracingService + PluginTelemetry).
var logger = serviceProvider.GetLogger(
ServiceProviderExtensions.LogSink.PluginTelemetry,
ServiceProviderExtensions.LogSink.TracingService
);
logger.LogInformation("Processing entity {0}", entityId);
logger.LogWarning("Warning: {0}", message);
logger.LogError("Error occurred: {0}", exception);Log Sinks:
PluginTelemetry is intentionally named as a separate sink because Dataverse's telemetry logger is not Microsoft.Extensions.Logging.ILogger.
If your environment enables Dataverse Plugin Telemetry / Application Insights integration, you can keep the same ILogger calls and only adjust selected sinks.
When calling GetLogger(params LogSink[] sinks):
- no sinks defaults to
TracingService - one sink logs to that sink
- multiple sinks write to all specified sinks
// Logs to TracingService only
var logger1 = serviceProvider.GetLogger(ServiceProviderExtensions.LogSink.TracingService);
// Logs to PluginTelemetry only
var logger2 = serviceProvider.GetLogger(ServiceProviderExtensions.LogSink.PluginTelemetry);
// Logs to both
var logger3 = serviceProvider.GetLogger(
ServiceProviderExtensions.LogSink.PluginTelemetry,
ServiceProviderExtensions.LogSink.TracingService
);Simplified access to execution context data with type-safe casting:
using Digitall.Plugins.Extensions;
var context = serviceProvider.GetExecutionContext();
// Type-safe entity retrieval
var account = context.GetTarget<Account>();
var accounts = context.GetTargets<Account>(); // For bulk operations
// Image retrieval with casting
var preImage = context.GetPreImage<Account>();
var preImages = context.GetPreImages<Account>(); // For bulk operations
var postImage = context.GetPostImage<Account>();
var postImages = context.GetPostImages<Account>(); // For bulk operations
// Query information
var columnSet = context.GetColumnSet();
var isQuery = context.GetQuery(out QueryExpression query, out ColumnSet columns);
// Formatted context information
var stageName = context.GetFormattedExecutionStage(); // e.g., "PreOperation"
var modeName = context.GetFormattedExecutionMode(); // e.g., "Synchronous"
// Entity relationship operations
var relationship = context.GetRelationship();
var relatedEntities = context.GetRelatedEntities();Abstract base class implementing IPlugin with built-in logging and exception handling.
| Member | Type | Description |
|---|---|---|
ExecuteInternal(serviceProvider) |
abstract void |
Implement your plugin logic here |
Abstract base class implementing IPlugin with simplified property access and stateless execution.
| Member | Type | Description |
|---|---|---|
Result |
ExecutionResult |
Get/set execution result |
Core |
IPluginExecutionContext |
Raw execution context |
Entity |
Entity |
Target entity (single operations) |
GetEntities() |
EntityCollection |
Target entities (bulk operations) |
PreEntityImage |
Entity |
Pre-image entity |
GetPreEntityImages() |
EntityCollection |
Pre-image entities (bulk) |
PostEntityImage |
Entity |
Post-image entity |
GetPostEntityImages() |
EntityCollection |
Post-image entities (bulk) |
SecuredOrganizationService |
IOrganizationService |
Cached service for current user |
ElevatedOrganizationService |
IOrganizationService |
Cached service with system privileges |
Execute() |
abstract ExecutionResult |
Implement your plugin logic here |
Available on IServiceProvider:
GetExecutionContext() // IPluginExecutionContext7
GetOrganizationService() // IOrganizationService (current user)
GetOrganizationService(Guid userId) // IOrganizationService (specific user)
GetElevatedOrganizationService() // IOrganizationService (system)
GetTracingService() // ITracingService
GetLogger() // Microsoft.Xrm.Sdk.PluginTelemetry.ILogger
GetLogger(params LogSink[] sinks) // Microsoft.Extensions.Logging.ILogger
GetTimeProvider() // TimeProvider
GetManagedIdentityService() // IManagedIdentityServiceAvailable on IPluginExecutionContext:
GetTarget<T>() // T (typed entity)
GetTargets<T>() // IEnumerable<T> (bulk operations)
GetPreImage<T>() // T (typed pre-image)
GetPreImages<T>() // IEnumerable<T> (bulk pre-images)
GetPostImage<T>() // T (typed post-image)
GetPostImages<T>() // IEnumerable<T> (bulk post-images)
GetColumnSet() // ColumnSet
GetQuery(out QueryExpression, out ColumnSet) // bool
GetQuery(out QueryByAttribute, out ColumnSet) // bool
GetQuery(out FetchExpression, out ColumnSet) // bool
GetRelationship() // Relationship
GetRelatedEntities() // EntityReferenceCollection
GetFormattedExecutionStage() // string ("PreOperation", etc.)
GetFormattedExecutionMode() // string ("Synchronous", etc.)
GetInputParameter<T>(string key, out T value) // bool
GetOutputParameter<T>(string key, out T value) // bool
SetOutputParameter<T>(string key, T value) // void
GetRetrieveEntity() // Entity
GetRetrieveMultipleEntities() // EntityCollectionAvailable on Entity:
HasAttribute(string attributeName) // bool
GetAttributeValue<T>(string attributeName, T defaultValue) // T
GetAttribute<T>(string attributeName) // TReturn value from Executor.Execute():
| Value | Description |
|---|---|
Ok |
Execution completed successfully |
Failure |
Execution failed |
Skipped |
Execution was skipped |
See the examples/SamplePlugin directory for complete working examples:
- VanillaSample.cs: Traditional IPlugin implementation
- SkeletonSample.cs: Using PluginSkeleton base class
- ExecutorSample.cs: Using Executor base class
- .NET Framework 4.6.2 or higher
- Microsoft.Xrm.Sdk 9.0 or higher
- Microsoft.Extensions.Logging 6.0 or higher
Released under the Microsoft Public License (MS-PL).