Skip to content

14 Migration from Daxif

RemyDuijkeren edited this page Jul 17, 2026 · 3 revisions

Migration from Daxif

Daxif (Delegate Automated Xrm Installation Framework) is an F# library from Delegate A/S that provides a programmable Dataverse deployment pipeline via F# scripts. It is in maintenance mode (last meaningful release 2024) and requires F# tooling and knowledge to operate.

Flowline covers the same core operations — plugin registration, web resource sync, solution pack/import, early-bound type generation — as a conventional CLI tool with no F# knowledge required.


What maps to what

Daxif Flowline
Plugin.Sync(env, dll, "", solution) flowline push --scope plugins
WebResource.Sync(env, folder, solution) flowline push --scope webresources
Plugin + web resource sync flowline push
Solution.Export(env, solution, outDir) flowline sync
Solution.Import(env, zipPath) flowline deploy test / flowline deploy prod
Solution.UpdateVersionNumber(...) Built into flowline sync --bump
Solution.Extract(...) Built into flowline clone / flowline sync
Solution.Pack(...) Built into flowline deploy
Solution.GenerateCSharpContext(...) flowline generate --generator xrmcontext3
Info.RetrieveVersion(env) flowline status
_Config.fsx environment definitions .flowline config file
Windows Credential Manager / OAuth config pac auth create
ExtendedSolution (pre/post import) Pre-import orphan cleanup built into flowline deploy (automatic) — see Extended Solution

Before you start

  • Install PAC CLI: dotnet tool install --global Microsoft.PowerApps.CLI.Tool (Windows alternative: winget install Microsoft.PowerAppsCLI)
  • Install Flowline: dotnet tool install --global Flowline
  • Authenticate: pac auth create --environment https://your-org.crm4.dynamics.com
  • Verify: flowline status

Two-phase migration strategy

Running everything in one pass is possible, but splitting the migration into two phases reduces risk — only one thing changes at a time, so problems are easier to diagnose.

Phase 1 — Standalone (no project restructuring required)

Replace Daxif for plugin registration, web resource sync, and type generation using Flowline's standalone mode. No .flowline config, no folder restructuring — Flowline reads your existing build output directly. Run these from a folder that does not contain a .flowline file.

# Plugins only
flowline push MySolution --pluginFile ./bin/Release/MyPlugins.dll --dev https://your-org.crm4.dynamics.com

# Web resources only
flowline push MySolution --webresources ./WebResources/dist --dev https://your-org.crm4.dynamics.com

# Both at once
flowline push MySolution --pluginFile ./bin/Release/MyPlugins.dll --webresources ./WebResources/dist --dev https://your-org.crm4.dynamics.com

# Type generation
flowline generate MySolution --namespace MyNamespace --output ./Models --dev https://your-org.crm4.dynamics.com

Always verify with --dry-run first to confirm Flowline's plan matches what Daxif was registering. Once phase 1 is stable, Daxif is no longer needed for day-to-day pushes.

Phase 2 — Project structure (when you're ready)

Run flowline init to create the .flowline config and adopt the Flowline folder convention (Plugins/, WebResources/ at the project root — no wrapper folder). From this point, flowline push reads from the project rather than requiring explicit flags.

The steps below cover Phase 2 in detail.


Step 1 — Replace plugin registration

This is the most significant migration task. Daxif requires plugins to inherit from its Plugin base class and declare steps in the constructor via a fluent API. Flowline uses attributes and detects any class that implements IPlugin — directly or through a base class — anywhere in its inheritance chain. Your own PluginBase : IPlugin works fine; Flowline will pick it up.

From fluent registration to attributes

// Daxif — Plugin base class + fluent API in constructor
public class MyAccountPlugin : Plugin
{
    public MyAccountPlugin()
    {
        RegisterPluginStep<Account>(EventOperation.Update, ExecutionStage.PostOperation, Execute)
            .SetExecutionMode(ExecutionMode.Asynchronous)
            .AddFilteredAttributes(x => x.Address1_City, x => x.Address1_Country)
            .AddImage(ImageType.PreImage, x => x.Address1_Country);
    }

    private void Execute(LocalPluginContext ctx) { ... }
}

// Flowline — attributes on any class that implements IPlugin (directly or via a base class)
[Step("account")]
[Filter("address1_city", "address1_country")]
[PreImage("address1_country")]
public class AccountPostUpdateAsyncPlugin : IPlugin
{
    public void Execute(IServiceProvider sp) { ... }
}

Fluent API → attribute mapping

Daxif fluent API Flowline equivalent
RegisterPluginStep<Account>(...) [Step("account")]
EventOperation.Update Class name: ...Update...
ExecutionStage.PreValidation Class name: ...Validation...
ExecutionStage.PreOperation Class name: ...Pre...
ExecutionStage.PostOperation Class name: ...Post...
SetExecutionMode(Asynchronous) Class name: ...Async... suffix
AddFilteredAttributes(x => x.Field) [Filter("field")]
AddImage(ImageType.PreImage, x => x.Field) [PreImage("field")]
AddImage(ImageType.PostImage, x => x.Field) [PostImage("field")]
SetExecutionOrder(1) [Step(..., Order = 1)]
SetImpersonatingUser(guid) [Step(..., RunAs = "guid")]
SetDeployment(ServerOnly) Not needed — Flowline targets online only
SetDescription(...) Not mapped

Class naming

Stage and message come from the class name. See 04-Push-Plugins-and-Custom-APIs for the full naming reference.

Daxif stage + message Flowline class name
PostOperation + Update + async AccountPostUpdateAsyncPlugin
PreOperation + Update AccountPreUpdatePlugin
PostOperation + Create AccountPostCreatePlugin
PreValidation + Delete ContactValidationDeletePlugin

Can't rename the class? Use [Handles] to declare stage and message explicitly — the class name is irrelevant:

[Step("account")]
[Handles(Message.Update, Stage.PreOperation)]
public class AccountPlugin : IPlugin { ... }

One class, multiple step registrations

Daxif's fluent API lets one constructor register multiple steps from one class:

// Daxif — two registrations from one class
public class AccountPlugin : Plugin
{
    public AccountPlugin()
    {
        RegisterPluginStep<Account>(EventOperation.Create, ExecutionStage.Post, Execute);
        RegisterPluginStep<Account>(EventOperation.Update, ExecutionStage.Post, Execute)
            .AddFilteredAttributes(x => x.Name, x => x.CreditLimit);
    }

    private void Execute(LocalPluginContext ctx) { ... }
}

Flowline's convention requires one class per step. During migration, stack [Handles] to register multiple steps without restructuring:

// Flowline — temporary multi-step form using stacked [Handles]
[Step("account")]
[Filter("name", "creditlimit")]
[Handles(Message.Create, Stage.PostOperation)]
[Handles(Message.Update, Stage.PostOperation)]
public class AccountPlugin : IPlugin { ... }

Flowline emits a warning (once per class) on every push nudging you to split into named subclasses. The long-term form:

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

[Step("account")]
[Filter("name", "creditlimit")]
public class AccountPostUpdatePlugin : IPlugin { ... }

Splitting warning: splitting the class renames the Dataverse steps. The next flowline push deletes the old steps and creates new ones. Plan the split for a maintenance window to avoid step downtime.

Custom APIs

Daxif uses a CustomAPI base class. Flowline uses attributes:

// Daxif
public class GetAccountRiskApi : CustomAPI
{
    public GetAccountRiskApi()
        : base(typeof(GetAccountRiskApi))
    {
        RegisterCustomAPI("GetAccountRiskApi", CalculateRisk)
            .EnableCustomization()
            .AddRequestParameter(new CustomAPIConfig.CustomAPIRequestParameter(
                "accountId", RequestParameterType.EntityReference, "account"))
            .AddResponseProperty(new CustomAPIConfig.CustomAPIResponseProperty(
                "riskScore", RequestParameterType.Integer));
    }

    protected void CalculateRisk(LocalPluginContext localContext)
    {
        var pluginContext = localContext.PluginExecutionContext;
        var accountRef = (EntityReference)pluginContext.InputParameters["accountId"];
        // ... calculate risk ...
        pluginContext.OutputParameters["riskScore"] = 42;
    }
}

// Flowline
[CustomApi]
[Input("accountId", FieldType.EntityReference, Table = "account")]
[Output("riskScore", FieldType.Integer)]
public class GetAccountRiskApi : IPlugin
{
    public void Execute(IServiceProvider sp) { ... }
}

If a Custom API is already live under a name that doesn't match what Flowline's class-name convention would derive, use UniqueName to pin the exact same value — Flowline validates it against the solution's publisher prefix and updates the existing record in place instead of creating a duplicate:

// Live Custom API is "dev1_ApproveOrder", but the class doesn't derive to that name
[CustomApi(UniqueName = "dev1_ApproveOrder")]
public class LegacyOrderApprovalPlugin : IPlugin { ... }

See 04-Push-Plugins-and-Custom-APIs#custom-apis for the full attribute reference.

Install Flowline.Attributes

Add to your Plugins project:

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

Remove the Daxif NuGet package from your project once all registrations are converted.

Important: plugins not inheriting from Daxif's Plugin base class

Daxif silently ignores any IPlugin class that does not inherit from its Plugin base class. If your project has plugins that bypass the base class, they are not registered by Daxif today — but Flowline will pick them up (any IPlugin implementor is eligible). Review for unintended registrations after the first flowline push --dry-run.


Step 2 — Migrate web resources

The naming convention is the same in both tools:

{publisher_prefix}_{solution_name}/{relative_path}

Example: dg_MySolution/js/utils.js

The difference is where files live on disk. Daxif scans a folder that already contains the full prefixed path. Flowline scans WebResources/dist/ and prepends the prefix and solution name:

Daxif:    webResources/dg_MySolution/js/utils.js  →  dg_MySolution/js/utils.js
Flowline: WebResources/dist/js/utils.js            →  dg_mysolution/js/utils.js

Migrating files

Move your web resource files from the Daxif folder into WebResources/dist/, dropping the {prefix}_{solution}/ prefix from the path:

Before: webResources/dg_MySolution/js/utils.js
After:  WebResources/dist/js/utils.js

Flowline derives the Dataverse name automatically from .flowline. Run flowline push --scope webresources --dry-run to confirm the names match before going live.

Note on case: Daxif preserves the exact folder name as the unique name prefix. Flowline lowercases the solution name. If your existing Dataverse records use mixed case (e.g. dg_MySolution/...) and Flowline generates dg_mysolution/..., treat this as a rename — see the web resource renaming guidance in 13-Migration-from-spkl#renaming-existing-dataverse-records.

See 08-WebResources-Project for a full guide to the TypeScript setup, Rollup build pipeline, and folder structure.


Step 3 — Replace early-bound type generation

Daxif uses XrmContext (a Delegate A/S tool) for C# generation and XrmDefinitelyTyped for TypeScript:

// Daxif — F# script
Solution.GenerateCSharpContext(Env.dev, pathToXrmContext, outputDir,
    solutions = ["MySolution"],
    entities = ["account"; "contact"])

Flowline wraps pac modelbuilder build:

flowline generate --namespace MySolution.Models --extra-tables account,contact

Generated types land in Plugins/Models/. The namespace and extra tables are saved to .flowline after the first run — flowline generate on subsequent runs needs no arguments.

TypeScript generation (XrmDefinitelyTyped) has no Flowline equivalent. Continue using XrmDefinitelyTyped directly via pac modelbuilder or standalone tooling for TypeScript type generation.


Step 4 — Replace solution operations

Export and sync

// Daxif — export + unpack
Solution.UpdateVersionNumber(Env.dev, solution, VersionIncrement.Revision)
Solution.Export(Env.dev, solution, outputDir, managed = false)
Solution.Extract(Path.Daxif.unmanagedSolution, customizationsFolder, xmlMappingFile, projFile)

// Flowline — one command
flowline sync --bump patch

Import and deploy

// Daxif
Solution.Pack(outputPath, customizationsFolder, xmlMappingFile, managed = false)
Solution.Import(Env.test, pathToSolutionZip, activatePluginSteps = true, publishAfterImport = true)

// Flowline
flowline deploy test
flowline deploy prod

Step 5 — Replace authentication

Daxif stores credentials in a local .daxif file or Windows Credential Manager and requires F# config:

// Daxif — _Config.fsx
let creds = Credentials.FromKey("UserCreds")
let dev = Environment.Create(
    name = "Development",
    url = "https://mydev.crm4.dynamics.com/XRMServices/2011/Organization.svc",
    ap = AuthenticationProviderType.OnlineFederation,
    creds = creds,
    method = ConnectionType.OAuth)

Flowline reuses the PAC CLI token cache:

# Interactive / MFA
pac auth create --environment https://mydev.crm4.dynamics.com

# CI/CD — service principal
pac auth create --kind ServicePrincipal --applicationId $CLIENT_ID --clientSecret $CLIENT_SECRET --tenant $TENANT_ID

Run flowline status to verify the active auth profile.


Step 6 — Replace F# scripts

Daxif operations are invoked by executing .fsx scripts:

fsi.exe PluginSyncDev.fsx
fsi.exe WebResourceSyncDev.fsx
fsi.exe SolutionExportDev.fsx

Replace with Flowline CLI calls:

Daxif script Flowline command
PluginSyncDev.fsx flowline push --scope plugins
WorkflowSyncDev.fsx flowline push --scope plugins (workflow activities in same assembly)
WebResourceSyncDev.fsx flowline push --scope webresources
SolutionExportDev.fsx flowline sync
SolutionImportArg.fsx flowline deploy <target>
SolutionExtract.fsx flowline clone (first time) / flowline sync
SolutionPack.fsx Built into flowline deploy
GenerateCSharpContext.fsx flowline generate
SolutionCreateDev.fsx flowline init (coming v1)

Extended Solution — what's covered, what's not

Daxif's Extended Solution feature wraps solution import in three phases: pre-import orphan cleanup, standard import, and post-import state restoration. This solves the "additive-only import" problem inherent to Dataverse unmanaged solution import.

Extended Solution phase Flowline equivalent
PreImport — delete plugin steps, assemblies, web resources removed from source Built into flowline deploy, automatic — deploy diffs the target against local Solution.xml before import and deletes what's gone. Anything blocked by a dependency retries right after import. Pass --no-delete to only report, not delete. See 07-Deploy#orphan-cleanup.
PostImport — restore workflow statecode/statuscode Not yet supported
PostImport — reassign workflow owners across environments Not planned

flowline push is not affected — direct push handles its own orphan cleanup independently of solution import.


Known gaps

Data export / import

Daxif has a full data migration module (Data.Export, Data.Import) with GUID remapping, owner reassignment, delta export, and N:N association import. Flowline has no equivalent — it is intentionally focused on the ALM pipeline, not data migration. Use the Configuration Migration Tool or dataverse-utils for data migration.

Enable / disable plugin steps

Daxif supports enabling or disabling individual plugin steps without a full sync:

Plugin.EnableSolutionPluginSteps(env, solution, enable = false)

Flowline has no equivalent. Use Plugin Registration Tool or a direct Dataverse API call for environment-specific step activation.

TypeScript type generation

Daxif generates TypeScript context via XrmDefinitelyTyped. Flowline's flowline generate produces C# early-bound types only via pac modelbuilder build. Continue using XrmDefinitelyTyped directly for TypeScript.

Clone this wiki locally