Metadata Attributes - rezanid/xrmtools GitHub Wiki
Xrm Tools uses a set of custom attributes (provided by the XrmTools.Meta.Attributes NuGet package) to let you declare how your plugin should be registered in Dataverse. By adding these attributes to your classes (and sometimes nested classes), you describe the Plugin Registration settings in code. Xrm Tools reads these attributes to perform automated registration and to generate useful code (like strongly-typed entity images and targets).
After installing the XrmTools.Meta.Attributes package, you’ll have access to these key attributes:
-
[Plugin] – Marks a class as a plugin. Every plugin class must have this. It indicates to Xrm Tools (and to you, the developer) that this class represents a plugin that will be registered. The
[Plugin]
attribute should be placed above all other registration attributes on the class. It can optionally include properties likeDescription
or Name (if not provided, the plugin’s name defaults to the class name). -
[Step] – Defines a plugin step (SdkMessageProcessingStep) for a plugin class. This attribute takes parameters such as the message name (e.g., “Create”, “Update”, “Delete”), the primary entity logical name it applies to, optional filtering attributes, the stage (PreValidation, PreOperation, PostOperation), and execution mode (Synchronous or Asynchronous). For example:
[Plugin] [Step("Create", "contact", "firstname,lastname", Stages.PostOperation, ExecutionMode.Synchronous)] public partial class ContactCreatePlugin : IPlugin { }
In this example, we declare that
ContactCreatePlugin
should run on the Create message of the contact entity, and only when the attributes “firstname” or “lastname” are in the input (filtering attributes). The plugin will run in the PostOperation stage, synchronously. You can apply multiple[Step]
attributes to the same class if the plugin needs to handle multiple messages or entities. -
[Image] – Defines a step image (Pre or Post image) for the preceding
[Step]
. An image captures the state of the entity either before or after the operation and passes it to the plugin. The attribute parameters include the image type (PreImage or PostImage) and the list of attributes to include in the image. Example:[Image(ImageTypes.PostImage, "firstname,lastname")]
. This would register a Post-Image on the step, including the First Name and Last Name in that image. Like Step, you can have multiple images (multiple [Image] attributes) after a Step if needed, though typically one Pre and/or one Post image is used. -
[CustomApi] – Marks a class as a Custom API plugin. Custom APIs are a way to define new messages (operations) in Dataverse. You can read about Writing-a-Custom-API, but in short: if your plugin is meant to register a new message, use
[CustomApi]
on the class (in addition to[Plugin]
). The attribute parameters define the unique name, display name, binding type (Global, Entity, EntityCollection), etc., for the Custom API. A class with[CustomApi]
typically will not have[Step]
attributes, because it’s defining a custom operation rather than hooking into an existing one. -
[CustomApiRequest] – Applied to a nested class inside a plugin class that has
[CustomApi]
. The nested class represents the request parameters for the Custom API. All public properties of this inner class become the input parameters of the Custom API (when you register it, Xrm Tools will create corresponding request parameter definitions in Dataverse). You can only have one class marked with[CustomApiRequest]
inside a given plugin class. If a property of the request class is a nullable type (e.g. int?, string?), it will be treated as an optional parameter (not required when the API is called). Non-nullable (or marked required in C# 11) properties will be treated as required parameters. -
[CustomApiResponse] – Similar to
CustomApiRequest
, but for defining output. Apply this to one nested class inside your plugin class to represent the response returned by your Custom API. Each public property on this class becomes an output property of the Custom API. You can only have one[CustomApiResponse]
class per plugin class.
Using these attributes makes the plugin’s purpose and requirements very clear in code. Consider a simple example of a plugin with these attributes:
[Plugin]
[Step("Create", "contact", "firstname,lastname", Stages.PostOperation, ExecutionMode.Synchronous)]
[Image(ImageTypes.PostImage, "firstname,lastname")]
public partial class ContactCreatePlugin : IPlugin
{
public void Execute(IServiceProvider serviceProvider)
{
// ... plugin logic ...
}
}
This code succinctly indicates: When a Contact is created, after the operation, run this plugin, and provide a Post-Image with first name and last name. It’s much shorter and clearer than the equivalent XML or manual registration steps.
Because Xrm Tools knows your intentions from these attributes, it can do two major things:
-
Automatic Registration – You can deploy (register) the plugin with one click from Visual Studio (no need to manually configure steps in the Plugin Registration Tool). Xrm Tools reads the
[Plugin]
,[Step]
,[Image]
, etc., and registers exactly what you declared. -
Code Generation – Xrm Tools can generate additional code to make writing the plugin easier. For example, in the above plugin, since you declared a target entity (contact with certain attributes) and a post-image, Xrm Tools will generate:
- A
Target
property on your plugin class of typeContact
(a strongly-typed entity class) that has at least the FirstName and LastName properties. This represents the target entity of the operation, so you don’t have to manually cast or retrieve it from context. - A
PostImage
property on your plugin class, also of typeContact
, with FirstName and LastName, marked as read-only (since images should not be modified).
These generated properties allow you to access the input and images in a type-safe way, e.g.
Target.FirstName
instead of dealing withIPluginExecutionContext.InputParameters
. - A
[!Note]
If you mark a property in a CustomApi request class as nullable (int?, string?, etc.), Xrm Tools interprets that as an optional parameter in the registration. This means callers of the Custom API don’t need to supply it. Non-nullable (or marked required) properties will be required parameters.
In summary, the metadata attributes let you define what your plugin does and needs in a declarative way. Xrm Tools uses this information to streamline both the registration process and your coding experience.
More Examples
Example 1: Simple Plugin
using Microsoft.Xrm.Sdk;
using System;
using XrmTools.Meta.Attributes;
using XrmTools.Meta.Model;
namespace XrmGenTest;
[Plugin]
[Step("Create", "contact", "firstname,lastname", Stages.PostOperation, ExecutionMode.Synchronous)]
[Image(ImageTypes.PostImage, "firstname,lastname")]
public partial class ContactCreatePlugin : IPlugin
{
public void Execute(IServiceProvider serviceProvider)
{
if (serviceProvider == null)
{
throw new InvalidPluginExecutionException(nameof(serviceProvider));
}
Initialize(serviceProvider);
}
}
Example 2: Plugin with a base class
using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Extensions;
using System;
using XrmTools.Meta.Attributes;
using XrmTools.Meta.Model;
namespace XrmGenTest;
public abstract class PluginBase : IPlugin
{
public void Execute(IServiceProvider serviceProvider)
{
if (serviceProvider == null)
{
throw new InvalidPluginExecutionException(nameof(serviceProvider));
}
var executionContext = serviceProvider.Get<IPluginExecutionContext7>();
var organizationService = serviceProvider.GetOrganizationService(executionContext.UserId);
var tracing = serviceProvider.Get<ITracingService>();
Initialize(serviceProvider);
}
internal virtual void Initialize(IServiceProvider serviceProvider) { }
}
[Plugin]
[Step("Create", "account", "accountnumber,accountcategorycode,accountclassificationcode", Stages.PostOperation, ExecutionMode.Synchronous)]
[Image(ImageTypes.PostImage, "accountnumber")]
public partial class AccountCreatePlugin : PluginBase, IPlugin
{
public void ExecuteLocal(IServiceProvider serviceProvider)
{
// It's just business logic!
}
}
Example 3: Custom API
using Microsoft.Xrm.Sdk;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using XrmTools.Meta.Attributes;
namespace XrmGenTest;
[Plugin]
[CustomApi("test_MyCustomApi", "My Custom API", "MyCustomApi")]
public partial class MyCustomApiPlugin : IPlugin
{
public void Execute(IServiceProvider serviceProvider)
{
throw new InvalidPluginExecutionException("Test", PluginHttpStatusCode.ExpectationFailed);
}
[CustomApiRequest]
public class Request
{
public bool BooleanParameter { get; set; }
public DateTime DateTimeParameter { get; set; }
// Marking the type as nullable will make the parameter optional in the Custom API.
public decimal? DecimalParameter { get; set; }
public Entity EntityParameter { get; set; }
public EntityCollection EntityCollectionParameter { get; set; }
public EntityReference EntityReferenceParameter { get; set; }
public float FloatParameter { get; set; }
public int IntegerParameter { get; set; }
public Money MoneyParameter { get; set; }
public OptionSetValue PicklistParameter { get; set; }
public XrmTools.Meta.Model.BindingTypes EnumParameter { get; set; }
public string StringParameter { get; set; }
public string[] StringArrayParameter { get; set; }
public Guid GuidParameter { get; set; }
}
[CustomApiResponse]
public class Response
{
public bool BooleanParameter { get; set; }
public DateTime DateTimeParameter { get; set; }
public decimal DecimalParameter { get; set; }
public Entity EntityParameter { get; set; }
public EntityCollection EntityCollectionParameter { get; set; }
public EntityReference EntityReferenceParameter { get; set; }
public float FloatParameter { get; set; }
public int IntegerParameter { get; set; }
public Money MoneyParameter { get; set; }
public OptionSetValue PicklistParameter { get; set; }
public XrmTools.Meta.Model.BindingTypes EnumParameter { get; set; }
public string StringParameter { get; set; }
public string[] StringArrayParameter { get; set; }
public Guid GuidParameter { get; set; }
}
}