Every plugin implements IPlugin, with a single Execute method as the entry point. Here’s a minimal, real example: blocking an Account from being created without a Phone number.

public class ValidateAccountPhone : IPlugin
{
    public void Execute(IServiceProvider serviceProvider)
    {
        var context = (IPluginExecutionContext)serviceProvider
            .GetService(typeof(IPluginExecutionContext));

        if (context.InputParameters.Contains("Target") &&
            context.InputParameters["Target"] is Entity target)
        {
            if (!target.Contains("telephone1") ||
                string.IsNullOrWhiteSpace(target.GetAttributeValue<string>("telephone1")))
            {
                throw new InvalidPluginExecutionException(
                    "Phone number is required when creating an Account.");
            }
        }
    }
}

Walking through it

  • IServiceProvider serviceProvider — the single parameter every plugin’s Execute receives; every other service (context, tracing, organization service) is resolved from it.
  • IPluginExecutionContext — carries everything about the current operation: which message fired, the target entity, pre/post images, depth, and more.
  • context.InputParameters["Target"] — for Create and Update messages, this is the record being created/updated. It’s typed as object and needs casting to Entity.
  • throw new InvalidPluginExecutionException(...) — the correct way to fail a plugin and show the user a clean error message, covered properly in a later chapter.

Getting the organization service

To actually query or update other records (not just inspect the Target), you need an IOrganizationService, resolved like this:

var serviceFactory = (IOrganizationServiceFactory)serviceProvider
    .GetService(typeof(IOrganizationServiceFactory));
IOrganizationService service = serviceFactory.CreateOrganizationService(context.UserId);

Using context.UserId runs the service call as the user who triggered the operation (respecting their security), which is almost always what you want — using a different, elevated identity is a deliberate, specific choice, not a default.

Dependency injection, by hand. Plugins don’t use a modern DI container — every dependency is manually pulled from serviceProvider.GetService(typeof(...)). This pattern looks unusual compared to current .NET conventions, but it’s the actual, correct way the platform expects a plugin to be written.