Flowline.Attributes 0.2.0

There is a newer version of this package available.
See the version list below for details.
dotnet add package Flowline.Attributes --version 0.2.0
                    
NuGet\Install-Package Flowline.Attributes -Version 0.2.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Flowline.Attributes" Version="0.2.0">
  <PrivateAssets>all</PrivateAssets>
  <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Flowline.Attributes" Version="0.2.0" />
                    
Directory.Packages.props
<PackageReference Include="Flowline.Attributes">
  <PrivateAssets>all</PrivateAssets>
  <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Flowline.Attributes --version 0.2.0
                    
#r "nuget: Flowline.Attributes, 0.2.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Flowline.Attributes@0.2.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Flowline.Attributes&version=0.2.0
                    
Install as a Cake Addin
#tool nuget:?package=Flowline.Attributes&version=0.2.0
                    
Install as a Cake Tool

Flowline.Attributes

Source-only NuGet package that provides attributes for registering Dataverse plugin steps with the Flowline CLI.

When you run flowline push, Flowline inspects your plugin assembly and automatically creates or updates the plugin step registrations in Dataverse — no Plugin Registration Tool needed.

Installation

<PackageReference Include="Flowline.Attributes" Version="1.0.0" PrivateAssets="all" />

PrivateAssets="all" keeps this as a development-time dependency. The attributes compile directly into your plugin assembly — no extra DLL is added to the output, which keeps the Dataverse sandbox happy.

How it works

Flowline detects a registerable plugin step by combining two things:

  1. Class name convention — encodes the message, stage, and processing mode
  2. [Entity] attribute — specifies the Dataverse entity (table) logical name

The naming pattern is:

{DescriptiveName}{Stage}{Message}[Async][Plugin]

The Plugin suffix is recommended but not required. Flowline strips it before scanning for keywords, so AccountPreUpdate and AccountPreUpdatePlugin are detected identically. The suffix helps readability in the IDE and in Plugin Registration Tool.

Class name segment Maps to Value
Validate or Validation ProcessingStage.PreValidation 10
Pre ProcessingStage.PreOperation 20
Post ProcessingStage.PostOperation 40
Create, Update, Delete, ... MessageName
Async (before Plugin suffix) ProcessingMode.Asynchronous
(absent) ProcessingMode.Synchronous

If no stage keyword or no [Entity] attribute is found, Flowline skips the class.

The "1 plugin = 1 step" rule

Each plugin class registers exactly one step. This is an intentional design decision enforced by Flowline. Here is why it makes your life easier in the long run.

Clearer telemetry and error logs When a plugin throws, Dataverse logs the class name. If one class handles both Create and Update, your logs say AccountPlugin — you still have to look at the context to know what triggered it. With one class per step, the log says AccountPostCreatePlugin — the failure is self-describing.

Simpler Execute bodies Without the rule, Execute fills up with branching logic:

public void Execute(IServiceProvider sp)
{
    var context = ...;
    if (context.MessageName == "Create") { ... }
    else if (context.MessageName == "Update") { ... }
}

With the rule, every Execute does exactly one thing. No branching, no defensive checks on MessageName or Stage — Dataverse guarantees which step fired because you registered only one.

Easier unit testing Testing one class per step means one test class per plugin. Each test has a clear arrange/act/assert structure with no need to set up different MessageName values to hit different branches.

Unambiguous image and filter ownership When a class registers multiple steps, [Filter] and [Image] become ambiguous — which step do they belong to? With one class per step, every attribute on the class unambiguously belongs to that single step.

Shared logic still works — use a base class The rule does not mean duplicating code. When the same logic applies to multiple steps, put it in a base class and declare one leaf class per step:

public abstract class AccountSavePlugin : IPlugin
{
    public void Execute(IServiceProvider sp) { /* shared logic */ }
}

[Entity("account")]
public class AccountPreCreate : AccountSavePlugin { }

[Entity("account")]
public class AccountPreUpdate : AccountSavePlugin { }

This is the same pattern for multiple messages (Create + Update) and for multiple entities (same step firing on account, contact, and opportunity). One solution for all cases.

public abstract class AccountSavePlugin : IPlugin
{
    public void Execute(IServiceProvider sp) { /* shared logic */ }
}

[Entity("account")]
public class AccountPreCreatePlugin : AccountSavePlugin { }

[Entity("account")]
public class AccountPreUpdatePlugin : AccountSavePlugin { }

Attributes

[Entity] — required

Specifies the Dataverse entity logical name. Without this attribute, Flowline ignores the class for step registration.

[Entity("account")]
public class AccountPostCreatePlugin : IPlugin { ... }

[Entity("cr07982_invoice")]
public class InvoicePreUpdatePlugin : IPlugin { ... }

Optional named properties:

Property Type Default Maps to
Order int 1 Execution Order in Plugin Registration Tool
As ExecuteAs CallingUser Run in User's Context
Configuration string? null Unsecure Configuration

Order — controls ordering when multiple plugins are registered on the same step. Lower numbers run first.

ExecuteAs — controls context.UserId inside Execute. Use InitiatingUser when a Flow or workflow triggers your plugin and you need the human user's context rather than the service account that owns the automation:

[Entity("account", As = ExecuteAs.InitiatingUser)]
public class AccountPostCreatePlugin : IPlugin
{
    public void Execute(IServiceProvider sp)
    {
        var context = (IPluginExecutionContext)sp.GetService(typeof(IPluginExecutionContext));
        // context.UserId is the human user who triggered the flow, not the flow service account
    }
}

Configuration — a plain string passed to your plugin constructor as the first parameter. Use it to supply endpoint URLs, feature flags, or serialized JSON config without hardcoding them:

[Entity("account", Configuration = "{\"endpoint\":\"https://my-service.example.com\"}")]
public class AccountPostCreatePlugin : IPlugin
{
    private readonly string _endpoint;

    public AccountPostCreatePlugin(string unsecureConfig)
    {
        _endpoint = JsonSerializer.Deserialize<Config>(unsecureConfig)!.Endpoint;
    }

    public void Execute(IServiceProvider sp) { ... }
}

Secure Configuration is intentionally not supported. Secrets should not be committed to source code. Use environment variables or Azure Key Vault instead.

Full example with all properties:

[Entity("account",
    Order = 2,
    As = ExecuteAs.InitiatingUser,
    Configuration = "{\"endpoint\":\"https://my-service.example.com\"}")]
public class AccountPreUpdatePlugin : IPlugin { ... }

[Filter] — optional

Limits the step to fire only when at least one of the listed attributes is included in the operation. Omit to fire on every change.

[Entity("account")]
[Filter("name", "telephone1")]
public class AccountPreUpdatePlugin : IPlugin { ... }

Use nameof with your early-bound generated context class for refactor-safe names:

[Entity("account")]
[Filter(nameof(Account.name), nameof(Account.telephone1))]
public class AccountPreUpdatePlugin : IPlugin { ... }

[Image] — optional, stackable

Registers a pre- or post-image snapshot on the step. Stack multiple times for both.

[Entity("account")]
[Image(ImageType.PreImage)]                                    // all attributes
public class AccountPreUpdatePlugin : IPlugin { ... }

[Entity("account")]
[Image(ImageType.PreImage, "name", "telephone1")]              // specific attributes
public class AccountPreUpdatePlugin : IPlugin { ... }

Use nameof here too:

[Entity("account")]
[Image(ImageType.PreImage, nameof(Account.name))]
public class AccountPreUpdatePlugin : IPlugin { ... }

The alias used to retrieve the image in code is derived from the ImageType — you don't set it explicitly:

var preImage = context.PreEntityImages["preimage"];
var postImage = context.PostEntityImages["postimage"];

If you need a custom alias (rare — only when stacking two images of the same type), pass it as the first argument:

[Image("beforeMerge", ImageType.PreImage, "name")]

Examples

Minimal — standard entity, no filter, no image

using Flowline.Attributes;
using Microsoft.Xrm.Sdk;

[Entity("account")]
public class AccountPostCreatePlugin : IPlugin
{
    public void Execute(IServiceProvider serviceProvider)
    {
        // your logic here
    }
}

PreValidation — validate before the operation commits

[Entity("account")]
[Filter("creditlimit")]
public class AccountValidateUpdatePlugin : IPlugin
{
    public void Execute(IServiceProvider serviceProvider)
    {
        var context = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext));
        var target = (Entity)context.InputParameters["Target"];

        if (target.Contains("creditlimit") && target.GetAttributeValue<Money>("creditlimit").Value > 100_000)
            throw new InvalidPluginExecutionException("Credit limit cannot exceed 100,000.");
    }
}

PostOperation async — fire after the record is saved

[Entity("cr07982_invoice")]
[Filter("cr07982_status")]
public class InvoicePostUpdateAsyncPlugin : IPlugin
{
    public void Execute(IServiceProvider serviceProvider)
    {
        // runs asynchronously after the update commits
    }
}

PostOperation with pre-image and post-image

[Entity("account")]
[Filter("name", "telephone1")]
[Image(ImageType.PreImage, "name", "telephone1")]
[Image(ImageType.PostImage, "name", "telephone1")]
public class AccountPostUpdatePlugin : IPlugin
{
    public void Execute(IServiceProvider serviceProvider)
    {
        var context = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext));
        var preImage = context.PreEntityImages["preimage"];
        var postImage = context.PostEntityImages["postimage"];

        var oldName = preImage.GetAttributeValue<string>("name");
        var newName = postImage.GetAttributeValue<string>("name");

        if (oldName != newName)
        {
            // name changed — react here
        }
    }
}

Same logic, multiple entities

public abstract class RelatedEntityPostCreatePlugin : IPlugin
{
    public void Execute(IServiceProvider serviceProvider)
    {
        // shared logic for all entity registrations
    }
}

[Entity("account")]
public class RelatedEntityPostCreateForAccountPlugin : RelatedEntityPostCreatePlugin { }

[Entity("contact")]
public class RelatedEntityPostCreateForContactPlugin : RelatedEntityPostCreatePlugin { }

[Entity("opportunity")]
public class RelatedEntityPostCreateForOpportunityPlugin : RelatedEntityPostCreatePlugin { }
There are no supported framework assets in this package.

Learn more about Target Frameworks and .NET Standard.

  • .NETStandard 2.0

    • No dependencies.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.4.0 103 5/7/2026
0.3.0 99 5/5/2026
0.2.0 134 4/19/2026